Documentation
¶
Overview ¶
Package models provides database model definitions for SolidPing.
Package models provides data models for the SolidPing database.
Index ¶
- Constants
- Variables
- func AbandonedResultThreshold(checkPeriod time.Duration) time.Duration
- func CapBody(body []byte) string
- func ContactRequiresVerification(contactType string) bool
- func EventTypeLikePattern(prefix string) string
- func HideBrandingSectionPatch(hide bool) string
- func HideBrandingSettingsPatch(hide bool) string
- func IsActiveAt(window *MaintenanceWindow, target time.Time) bool
- func IsValidReportFrequency(frequency string) bool
- func LikeContainsPattern(value string) string
- func MigrateRegionList(list []string, from, target string) ([]string, bool)
- func RawAvailability(results []*Result) (int, int)
- func ResultColumnsWithoutBlobs(alias string) []string
- func RollupPageStatus(resources []PageResourceStatus) (PageStatus, PageStatusCounts)
- func StateKey(parts ...string) string
- func Status(window *MaintenanceWindow, now time.Time) string
- func StatusToString(status int) string
- func TruncateSupportBody(body string) (string, bool)
- func ValidCustomDomainState(s string) bool
- func ValidStatusPageVisibility(v string) bool
- func ValidSupportChannel(channel string) bool
- func ValidSupportStatus(status string) bool
- func ValidateCapabilitySet(capabilities []string) error
- func VerifiableContactTypes() map[string]bool
- type ActorType
- type Agent
- type AgentEnrollmentToken
- type AgentNonce
- type AggregateResultsFunc
- type AppSetting
- type AutoResolvePolicy
- type AvailabilityCounts
- type AvailabilitySettings
- type BrandingSettings
- type Capabilities
- type CapabilityState
- type Check
- func (c *Check) EffectiveFlapCount(now time.Time) int
- func (c *Check) EffectiveRecoveryPeriod() time.Duration
- func (c *Check) EffectiveRecoveryPeriodAt(now time.Time) time.Duration
- func (c *Check) FlappingWindowElapsed(now time.Time) bool
- func (c *Check) RegionSpreadDuration() *time.Duration
- func (c *Check) TimeoutOrDefault(defaultTimeout time.Duration) time.Duration
- type CheckConnection
- type CheckConnectionUpdate
- type CheckDependency
- type CheckDependencyKind
- type CheckDependencyUpdate
- type CheckGroup
- type CheckGroupUpdate
- type CheckJob
- type CheckJobUpdate
- type CheckLabel
- type CheckRate
- type CheckStatus
- type CheckStatusCount
- type CheckUpdate
- type CompactResultsOutcome
- type ConnectionType
- type DeliveryDetails
- type DeviceAuthRequest
- type DeviceAuthResolution
- type DiscordSettings
- type DiscoveredCheck
- type DiscoverySource
- type EmailSuppression
- type EntitlementLimits
- type EntitlementSource
- type EntitlementsPayload
- type EscalationPolicy
- type EscalationPolicyStep
- type EscalationPolicyTarget
- type EscalationPolicyUpdate
- type EscalationTargetType
- type Event
- type EventType
- type File
- type FreeboxPrivateSettings
- type FreeboxSettings
- type Incident
- type IncidentClockUpdate
- type IncidentMemberCheck
- type IncidentMemberUpdate
- type IncidentNotification
- func NewIncidentNotificationForJob(...) *IncidentNotification
- func NewIncidentNotificationForUser(orgUID, incidentUID, eventType, source, userUID, channelType string, ...) *IncidentNotification
- func NewSkippedIncidentNotification(orgUID, incidentUID, eventType, source, skipReason string, stepUID *string, ...) *IncidentNotification
- type IncidentNotificationRow
- type IncidentPublication
- type IncidentPublicationUpdate
- type IncidentState
- type IncidentUpdate
- type Integration
- type IntegrationUpdate
- type JSONMap
- type Job
- type JobStatus
- type JobUpdate
- type KubernetesPrivateSettings
- type KubernetesSettings
- type Label
- type LabelSuggestion
- type ListChecksFilter
- type ListEventsFilter
- type ListFilesFilter
- type ListIncidentPublicationsFilter
- type ListIncidentsFilter
- type ListIntegrationsFilter
- type ListMaintenanceWindowsFilter
- type ListMembershipRequestsFilter
- type ListOrgEntitlementAuditsFilter
- type ListResultsFilter
- type ListResultsResponse
- type ListSLOsFilter
- type ListSeveritiesFilter
- type ListSupportThreadsFilter
- type MSTeamsBotSettings
- type MSTeamsDestination
- type MSTeamsSettings
- type MaintenanceWindow
- type MaintenanceWindowCheck
- type MaintenanceWindowUpdate
- type MemberRole
- type MembershipRequest
- type MembershipRequestStatus
- type OAuthClient
- type Occurrence
- type OnCallSchedule
- type OnCallScheduleOverride
- type OnCallScheduleUpdate
- type OnCallScheduleUser
- type OrgEntitlementAudit
- type OrgEntitlements
- type OrgUsageCounter
- type Organization
- type OrganizationMember
- type OrganizationMemberUpdate
- type OrganizationPreviousSlug
- type OrganizationProvider
- type OrganizationProviderUpdate
- type OrganizationUpdate
- type PageResourceStatus
- type PageStatus
- type PageStatusCounts
- type Parameter
- type ParameterUpdate
- type PeriodTierSide
- type ProviderType
- type PublicationSeverity
- type PublicationState
- type ReapAbandonedResultsOutcome
- type RecentResultsPerCheckFilter
- type RecentResultsTier
- type ReportSchedule
- type ReportScheduleUpdate
- type Result
- type ResultStatus
- type RotationType
- type SLO
- type SLOAlertPolicy
- type SLOAlertPolicyDefault
- type SLOAlertPolicyUpdate
- type SLOUpdate
- type SectionSelector
- type Severity
- type SeveritySeed
- type SeverityUpdate
- type SignupAttribution
- type SlackSettings
- type StateEntry
- type StatusPage
- type StatusPageBrandingUpdate
- type StatusPageCustomDomainUpdate
- type StatusPagePeriod
- type StatusPageResource
- type StatusPageResourceUpdate
- type StatusPageSection
- type StatusPageSectionUpdate
- type StatusPageSettings
- func (s StatusPageSettings) EffectiveThresholds() (float64, float64)
- func (s StatusPageSettings) FaviconFileUID() *string
- func (s StatusPageSettings) HideBranding() bool
- func (s StatusPageSettings) LogoFileUID() *string
- func (s *StatusPageSettings) Scan(value any) error
- func (s StatusPageSettings) Value() (driver.Value, error)
- type StatusPageSubscriber
- type StatusPageUpdate
- type StatusUpdate
- type StatusUpdateKind
- type StatusUpdatesFilter
- type SubscriberChannel
- type SubscriberScope
- type SupportMessage
- type SupportReplyWindow
- type SupportThread
- type TLSStorageEntry
- type TLSStorageKeyInfo
- type TLSStorageLock
- type TokenType
- type TwilioSettings
- type User
- type UserContact
- type UserIntegrationIdentity
- type UserNotificationRoute
- type UserPasskey
- type UserPasskeyUpdate
- type UserProvider
- type UserToken
- type UserTokenUpdate
- type UserUpdate
- type Worker
- type WorkerUpdate
Constants ¶
const ( // AgentStatusActive is an enrolled, usable agent. AgentStatusActive = "active" // AgentStatusRevoked is an agent an admin has revoked; it can no longer // authenticate and its X25519 key is excluded from future seals. AgentStatusRevoked = "revoked" )
Agent status values.
const ( // AgentKindOrg is a tenant-private deported agent: bound to exactly one // organization and one `@<org>/<region>` private region. AgentKindOrg = "org" // AgentKindSystem is a platform-operated agent (e.g. a fly.io machine) // serving a SHARED cloud region across every organization. It has no owning // org, and its claim scope is the region alone — the same scope the // in-cluster DirectBackend uses. AgentKindSystem = "system" )
Agent kinds (spec 2026-07-27-01).
const ( DeviceAuthStatusPending = "pending" DeviceAuthStatusApproved = "approved" DeviceAuthStatusDenied = "denied" )
Device authorization request statuses (RFC 8628 §3.3-3.5). A request starts pending and moves exactly once to approved or denied; the row is then consumed (hard-deleted) by the client's next poll.
const ( // EmailSuppressionSourceLink means the recipient followed the // unsubscribe link (either the RFC 8058 one-click POST or the GET // confirmation page). EmailSuppressionSourceLink = "link" // EmailSuppressionSourceHeader means an inbox provider auto-submitted the // List-Unsubscribe-Post one-click request on the recipient's behalf // without them visiting the confirmation page. Same code path as "link" // today (the POST handler cannot distinguish the two) — kept as a // distinct value for when/if that becomes distinguishable, and so the // audit trail's vocabulary already has a slot for it. EmailSuppressionSourceHeader = "header" // EmailSuppressionSourceDashboard means an org admin added the // suppression manually from the dashboard (not currently exposed by the // API — creation is always recipient-initiated — but modeled so a future // admin-initiated mute doesn't need a schema change). EmailSuppressionSourceDashboard = "dashboard" )
Email suppression source constants — where the suppression came from.
const ( ResolutionTypeAuto = "auto" ResolutionTypeManual = "manual" ResolutionTypeExpired = "expired" )
Resolution type values for the resolution_type column. Manual = closed by a human action; auto = the adaptive resolution logic decided the check is up again; expired = future use for time-based resolution.
const ( // IncidentKindCheck is a failing check — the original and default meaning, // so every pre-existing row keeps it with no backfill. IncidentKindCheck = "check" // IncidentKindSLOBurn is an SLO error-budget burn-rate alert (spec // 2026-08-21-08). It carries SLOUID + SLOAlertPolicyUID, and a // representative check in CheckUID purely so channel resolution and // escalation-policy resolution have an anchor to work from. IncidentKindSLOBurn = "slo_burn" )
Incident kinds. `kind` discriminates what an incident row is ABOUT. It is a discriminator, not a category: everything downstream (notification fan-out, escalation policies, ack/snooze/resolve, the timeline) is deliberately identical for both, which is the whole reason burn alerts are incidents rather than a parallel object.
const ( IncidentNotificationStatusPending = "pending" IncidentNotificationStatusSent = "sent" IncidentNotificationStatusFailed = "failed" IncidentNotificationStatusCanceled = "cancelled" //nolint:misspell // DB column uses British English IncidentNotificationStatusSkipped = "skipped" )
Status constants for IncidentNotification.
const ( IncidentNotificationSourceCheckConnection = "check_connection" IncidentNotificationSourceEscalationUser = "escalation_user" IncidentNotificationSourceEscalationSchedule = "escalation_schedule" IncidentNotificationSourceEscalationAllAdmins = "escalation_all_admins" IncidentNotificationSourceEscalationConnection = "escalation_connection" )
Source constants for IncidentNotification.
const ( // SlackCommentIngestionExplicit ingests only `/comment` invocations. SlackCommentIngestionExplicit = "explicit" // SlackCommentIngestionAll ingests every human thread reply. SlackCommentIngestionAll = "all" )
Slack comment-ingestion modes. See SlackSettings.CommentIngestion.
const ( // DiscordCommentIngestionExplicit ingests only `/solidping comment`. DiscordCommentIngestionExplicit = "explicit" // DiscordCommentIngestionAll ingests every human thread reply. DiscordCommentIngestionAll = "all" )
Discord comment-ingestion modes. Deliberately the same string values as the Slack ones so an operator reading two settings blobs sees one vocabulary.
const ( FreeboxStatusPairing = "pairing" FreeboxStatusGranted = "granted" FreeboxStatusDenied = "denied" FreeboxStatusTimeout = "timeout" )
Freebox pairing status values that live in FreeboxSettings.Status. They are declared here (rather than in the integrations/freebox package) so model callers — channel forms, list responses, audit logs — can branch on them without importing the higher-level integration package.
const ( // RecurrenceNone marks a one-off (non-recurring) window. RecurrenceNone = "none" // RecurrenceDaily repeats the window every day at the anchor's time-of-day. RecurrenceDaily = "daily" // RecurrenceWeekly repeats the window every week on the anchor's weekday. RecurrenceWeekly = "weekly" // RecurrenceMonthly repeats the window every month on the anchor's day-of-month. RecurrenceMonthly = "monthly" )
Maintenance window recurrence cadences.
const ( // MaintenanceStatusActive means a window occurrence covers now. MaintenanceStatusActive = "active" // MaintenanceStatusUpcoming means the window has future occurrences but none cover now. MaintenanceStatusUpcoming = "upcoming" // MaintenanceStatusPast means the window has no remaining occurrences. MaintenanceStatusPast = "past" )
Maintenance window lifecycle statuses (returned by Status).
const ( // UsageCounterKindSMS counts outbound SMS messages per month. UsageCounterKindSMS = "sms" // UsageCounterKindVoice counts outbound voice calls per month. UsageCounterKindVoice = "voice" // UsageCounterKindWhatsApp counts outbound WhatsApp template messages per // month. UsageCounterKindWhatsApp = "whatsapp" // UsageCounterKindCheckRateLimited counts check executions that were // deferred because the org exhausted its MaxChecksPerMinute budget. Unlike // the three kinds above it is a DAILY counter: PeriodStart is the UTC day, // because "were executions skipped today?" is the question the over-limit // banner answers, and a monthly bucket would keep the banner lit for weeks // after the org came back under its cap (spec 2026-08-26-03). UsageCounterKindCheckRateLimited = "check_rate_limited" )
Usage-counter kinds.
const ( // ReportFrequencyWeekly emits one report per ISO week (Monday start). ReportFrequencyWeekly = "weekly" // ReportFrequencyMonthly emits one report per calendar month. ReportFrequencyMonthly = "monthly" )
Report schedule frequencies.
const ( PeriodTypeRaw = "raw" PeriodTypeHour = "hour" PeriodTypeDay = "day" PeriodTypeMonth = "month" )
PeriodType values for the Result.PeriodType column.
const ( // AbandonedResultLeaseGrace mirrors the check_jobs lease formula // (scheduled_at + period + 30s, see checkjobsvc.Service.ClaimJobs): the // worst-case slack a legitimately in-flight attempt could still need on // top of the check's own period. AbandonedResultLeaseGrace = 30 * time.Second // AbandonedResultMultiplier pads (period + lease grace) generously so a // single routine worker restart mid-cycle is never mistaken for // abandonment. Only a check that has had no chance to produce ANY result // across several of its own periods is reaped. AbandonedResultMultiplier = 5 )
Constants behind AbandonedResultThreshold — see its doc comment.
const ( // SLOAlertPolicyKindFast is the "we are about to spend the month in an // afternoon" policy: a short long-window and a high threshold. SLOAlertPolicyKindFast = "fast" // SLOAlertPolicyKindSlow is the "this has been quietly eating the budget // all morning" policy: a longer window and a lower threshold. SLOAlertPolicyKindSlow = "slow" )
SLO alert policy kinds. There are exactly two built-ins — the product ships them, operators tune them, nobody invents a third from the API.
const ( // SLOAlertSeverityCritical is the fast-burn default. SLOAlertSeverityCritical = "critical" // SLOAlertSeverityWarning is the slow-burn default. SLOAlertSeverityWarning = "warning" )
Alert severities. Not stored on the incident — this codebase attaches severity to escalation-policy steps, not to incidents — but carried into the incident title, details and every notification body so the person paged can tell a fast burn from a slow one without opening the dashboard.
const ( StatusPageVisibilityPublic = "public" StatusPageVisibilityPrivate = "private" StatusPageVisibilityPassword = "password" )
Status page visibility values. `public` is world-readable, `private` is fully hidden (the public endpoints 404 as if it did not exist), and `password` is shared-with-a-secret: the public endpoints answer 401 until the visitor unlocks the page (spec 2026-08-21-07).
const ( // CustomDomainStateNone means no custom domain is configured. CustomDomainStateNone = "none" // CustomDomainStatePending means a domain is configured but has never // verified. The sweep NEVER auto-promotes out of this state — a first // verification is an operator action, so a hostname someone else parked a // CNAME on cannot bootstrap itself into being served. CustomDomainStatePending = "pending" // CustomDomainStateActive means verified and serving. CustomDomainStateActive = "active" // CustomDomainStateGrace means still verified and STILL SERVING, but // re-verification is currently failing. The common case (a transient DNS // fault) lives and dies here without a visitor ever noticing. CustomDomainStateGrace = "grace" // CustomDomainStateDemoted means the domain stayed unreachable well past // the grace window and verification was cleared. Recoverable: the sweep // re-promotes it after CustomDomainRepromoteSuccesses consecutive // successes while a valid certificate is still held. CustomDomainStateDemoted = "demoted" )
Custom-domain lifecycle states. Stored in status_pages.custom_domain_state.
The states are ordered by trust: none -> pending -> active, with grace as the "still serving but the re-checks are failing" holding pen and demoted as the terminal-until-recovered state. ONLY `demoted` has custom_domain_verified_at = NULL; `grace` keeps it set, and that is precisely what makes a DNS blip invisible to the page's visitors.
const ( // DefaultAvailabilityThresholdUp is the green floor: pct >= this is "up". DefaultAvailabilityThresholdUp = 99.9 // DefaultAvailabilityThresholdDegraded is the amber floor: pct >= this // (and below the up threshold) is "degraded"; below it is "down". DefaultAvailabilityThresholdDegraded = 99.0 )
Default availability color thresholds (spec 2026-08-03-01), used whenever a page's settings (or a section within it) don't specify a value. Badges (internal/handlers/badges) intentionally stay on these same numeric defaults without reading page settings — see that package's Decisions.
const ( // WireStatusCreated is the wire name for a just-created, not-yet-executed status. WireStatusCreated = "created" // WireStatusUp is the wire name for a healthy status. WireStatusUp = "up" // WireStatusDown is the wire name for a failing status. WireStatusDown = "down" // WireStatusValidating is the wire name for the transient pre-incident status. WireStatusValidating = "validating" // WireStatusDegraded is the wire name for the aggregated/summary degraded status. WireStatusDegraded = "degraded" // WireStatusWarning is the wire name for the live "up with something to report" status. WireStatusWarning = "warning" // WireStatusTimeout is the wire name for a timed-out status. WireStatusTimeout = "timeout" // WireStatusError is the wire name for an errored status. WireStatusError = "error" // WireStatusRunning is the wire name for an in-progress status. WireStatusRunning = "running" // WireStatusAbandoned is the wire name for a raw attempt the // abandoned-result reaper finalized: terminal, but excluded from every // availability calculation (spec 2026-08-18-10). WireStatusAbandoned = "abandoned" // WireStatusUnknown is the fallback wire name for an unrecognized status. WireStatusUnknown = "unknown" )
Lowercase status wire names, the single source of truth for the string form of CheckStatus (and the matching subtest/table labels in tests). These are the values the dashboard keys status colors and labels on. Centralizing them keeps any single literal from repeating across the package (goconst).
const ( SupportChannelWhatsApp = "whatsapp" SupportChannelTelegram = "telegram" SupportChannelSMS = "sms" SupportChannelSlack = "slack" SupportChannelDiscord = "discord" SupportChannelEmail = "email" )
Support channels. These are the inbound surfaces a human can reach us on. SupportChannelEmail exists in the vocabulary (and in the database CHECK constraint) but nothing writes it in v1: inbound email capture is deliberately a separate, later spec, so v1 ships the asymmetry "email support is a human mailbox, not a thread in the inbox". Reserving the value now means that spec needs no migration.
const ( SupportStatusOpen = "open" SupportStatusPending = "pending" SupportStatusClosed = "closed" )
Thread status. Set by the OPERATOR, deliberately — this is NOT the reply window. See SupportThread.ReplyWindow.
const ( SupportDirectionInbound = "inbound" SupportDirectionOutbound = "outbound" )
Message direction.
const ( SupportRawTypeText = "text" SupportRawTypeImage = "image" SupportRawTypeAudio = "audio" SupportRawTypeVideo = "video" SupportRawTypeDocument = "document" SupportRawTypeLocation = "location" SupportRawTypeSticker = "sticker" SupportRawTypeUnsupported = "unsupported" )
Raw message types. Anything the channel sends that is not plain text is recorded with a placeholder body and the real kind here, so an operator can see that a person sent a photo rather than seeing nothing at all.
const ( UserContactTypeEmail = "email" UserContactTypePhone = "phone" UserContactTypeSlackUser = "slack_user" UserContactTypePushover = "pushover_user" UserContactTypeNtfy = "ntfy_topic" UserContactTypeWebPush = "webpush" // UserContactTypeWhatsApp is a WhatsApp-reachable number. Deliberately // DISTINCT from UserContactTypePhone even when the digits are identical: // an SMS-verified number proves nothing about WhatsApp reachability, and // the WhatsApp verification round-trip doubles as Meta's required record // of the user's opt-in to receive business-initiated messages. Collapsing // the two would silently reuse an SMS consent as a WhatsApp consent. UserContactTypeWhatsApp = "whatsapp" // UserContactTypeTelegram is a Telegram chat id the instance bot can message. // The stored Value is the numeric chat id, never a @username: usernames can // be changed or reassigned, chat ids cannot. // // Deliberately NOT in VerifiableContactTypes: there is no code round-trip. // Pressing Start in Telegram (which delivers /start <token> to our webhook) // IS the proof of reachability and the opt-in, so the contact is created // already verified — and, correspondingly, a telegram contact may never be // created through the generic POST /notification-contacts endpoint, since // nothing there would stop a user typing a stranger's chat id. UserContactTypeTelegram = "telegram" )
UserContact type vocabulary.
const ( // IdentitySourceAuto marks an identity created by the email auto-match. IdentitySourceAuto = "auto" // IdentitySourceManual marks an identity an admin picked explicitly. IdentitySourceManual = "manual" )
Identity source vocabulary. `auto` rows were produced by the email auto-match (Slack `users.lookupByEmail`); `manual` rows were picked by an admin. A re-sync never overwrites a `manual` row — the admin's choice is the stronger statement about who this person is.
const ( // CapabilityIPv4 means the worker can originate IPv4 traffic. CapabilityIPv4 = "ipv4" // CapabilityIPv6 means the worker can originate IPv6 traffic. CapabilityIPv6 = "ipv6" // CapabilityBrowser means the worker can actually run a `browser` check: // a reachable remote Chrome (CDP) endpoint, or a local Chrome/Chromium // binary. Self-probed like the egress families, and advisory in the same // way — it drives a creation-time warning, never scheduling. CapabilityBrowser = "browser" )
Capability names, as stored verbatim in workers.capabilities and re-exported by internal/regions. They live here because they are the values the column holds, and because a producer (the worker self-probe, the agent claim path) must be able to name one without importing the region service.
Names are lowercase `[a-z0-9-]+` slugs; the database CHECK constraint and the SQLite triggers reject anything else.
const CheckConfigKeyTimeout = "timeout"
CheckConfigKeyTimeout is the check-config key holding the optional per-check execution timeout, stored as a Go duration string (spec 2026-07-11-05).
const DefaultAutoPublishDelaySeconds = 60
DefaultAutoPublishDelaySeconds is the debounce a new page starts with: an incident must still be open a minute later before customers hear about it.
const DefaultPublicationNotifyCap = 4
DefaultPublicationNotifyCap is how many subscriber fan-out waves one publication may trigger per hour before further updates are posted silently. Overridable per org via the `status_page.publication_notify_cap` parameter.
const DefaultReportTimezone = "UTC"
DefaultReportTimezone is the timezone a new schedule gets when none is given.
const DefaultSLOAlertMinSamples = 3
DefaultSLOAlertMinSamples is the per-window probe floor below which a window is inconclusive.
Deliberately low. A check whose period exceeds shortWindow/minSamples can never satisfy its own short window, and an alert policy that silently never fires is worse than one that occasionally fires on thin evidence — the operator can see and raise this number, but cannot see an alert that never happened.
const DefaultSLOTimezone = "UTC"
DefaultSLOTimezone is the timezone a new SLO gets when the caller supplies none.
const DeliveryDetailsBodyCap = 16 * 1024
DeliveryDetailsBodyCap is the maximum number of bytes retained for a captured request or response body before persistence. Bodies larger than this are truncated; the channel sender is responsible for capping before it builds the DeliveryDetails so secrets in oversized bodies are never marshaled.
const EntitlementsPayloadVersion = 1
EntitlementsPayloadVersion is the current schema version for the payload column. Future shape-breaking changes bump this and add a branch in EntitlementsPayload.UnmarshalJSON.
const EventTypeLikeEscape = `\`
EventTypeLikeEscape is the escape character the event-type family predicates pair with LIKE. It must be spelled the same way in every dialect's query.
const HeartbeatCounterTTL = 7 * 24 * time.Hour
HeartbeatCounterTTL is how long a heartbeat check's SP2 replay counter is kept after the last accepted beat.
It is a SLIDING window: every accepted signed beat pushes it out again, so a device that is actually beating never expires its own counter. Only a check that has stopped beating for this long lets its counter be swept, which is what keeps the store from accumulating counters for checks nobody uses any more.
Defined here, once, because both dialects bind it as a parameter — a duplicated interval literal in two .go files is exactly the kind of thing that drifts.
const IncidentNotificationChannelTypeNone = "none"
IncidentNotificationChannelTypeNone is used for skipped rows where no channel is involved.
const ParamKeyTracerouteEnabled = "diagnostics.traceroute.enabled"
ParamKeyTracerouteEnabled is the ORG-scoped switch that supplies the default for every check whose own `traceroute_on_failure` is NULL (spec 2026-08-21-10). Absent means ON.
It lives here, next to the parameter machinery, rather than in the incident package that reads it: the org-settings handler writes it and the incident pipeline reads it, and those two packages sit on opposite sides of an import cycle. A shared constant is what keeps the two ends from drifting apart into two subtly different strings.
const ParameterValueKey = "value"
ParameterValueKey is the single JSON key a scalar parameter value is stored under. Both engines write and read `{"value": …}`; naming it once keeps them from ever drifting.
const SectionSelectorMaxLabels = 10
SectionSelectorMaxLabels caps how many key=value pairs one selector may carry. Each pair becomes its own correlated subquery in ListChecks, so the cap is what stops a single section from authoring an arbitrarily expensive query.
const SectionSelectorMaxValueLen = 200
SectionSelectorMaxValueLen caps a selector label value, matching the label authoring cap in the dashboard (label-shared.ts VALUE_MAX).
const SlackScopeIMHistory = "im:history"
SlackScopeIMHistory is the bot scope Slack requires before it will deliver `message.im` — a direct message to the bot. Without it a DM is not merely ignored, it never arrives.
const SupportBodyMaxLength = 8000
SupportBodyMaxLength caps a stored message body. These endpoints are fed by publicly reachable phone numbers, so the body is attacker-influenced: over the cap it is truncated and flagged rather than rejected, because a truncated record still beats a lost one.
const WhatsAppReplyWindow = 24 * time.Hour
WhatsAppReplyWindow is the free customer-service window Meta opens when a user messages us. Inside it we may reply with ordinary text; outside it only an approved template may be sent, so a free-form reply is *impossible* rather than merely discouraged.
Variables ¶
var ( // ErrRecentResultsNoOrganization is returned when the filter names no // organization — every index on `results` leads with organization_uid. ErrRecentResultsNoOrganization = errors.New("recent results: organization uid is required") // ErrRecentResultsNoTiers is returned when the filter names no tier branch. ErrRecentResultsNoTiers = errors.New("recent results: at least one tier is required") // ErrRecentResultsMixedTier is returned when one branch names both raw and // a rollup tier (or names none at all). // // This is the guard that makes spec 2026-08-22-05's worst failure mode // unwritable. A per-check LATERAL/correlated fetch WITHOUT a tier // predicate does not fall back to the old plan — it becomes one sequential // scan of `results` PER CHECK, measured at 12 274 ms for a 20-check page // against 662 ms for the query it replaced. Both partial indexes on // `results` split on `period_type = 'raw'`, so a branch straddling the // split is implied by neither and can only be a scan. ErrRecentResultsMixedTier = errors.New( "recent results: each tier must sit entirely on one side of the raw/rollup index split") // ErrRecentResultsNoSince is returned when a tier carries no lower bound. ErrRecentResultsNoSince = errors.New("recent results: each tier needs a period_start lower bound") // ErrRecentResultsNoLimit is returned when no usable per-check budget is set. ErrRecentResultsNoLimit = errors.New("recent results: a positive default per-check limit is required") )
Errors returned by RecentResultsPerCheckFilter.Validate.
var ( // ErrSelectorEmpty is returned for `{}` — neither `all` nor `labels`. ErrSelectorEmpty = errors.New("selector must set either all or labels") // ErrSelectorAmbiguous is returned when both `all` and `labels` are set. ErrSelectorAmbiguous = errors.New("selector cannot set both all and labels") // ErrSelectorLabelsEmpty is returned for an empty `labels` object, which // would silently mean "every check" — the caller must say `all` for that. ErrSelectorLabelsEmpty = errors.New("selector labels must not be empty") // ErrSelectorTooManyLabels is returned above SectionSelectorMaxLabels. ErrSelectorTooManyLabels = errors.New("selector has too many labels") // ErrSelectorLabelKeyInvalid is returned for a key that no label could have. ErrSelectorLabelKeyInvalid = errors.New("selector label key is invalid") // ErrSelectorLabelValueInvalid is returned for an empty or over-long value. // Existence-only matching ("*") is deliberately NOT supported in v1. ErrSelectorLabelValueInvalid = errors.New("selector label value is invalid") )
Selector validation errors. They are returned to the API layer, which maps them onto VALIDATION_ERROR.
var ErrConflictingUserLimitKeys = errors.New(
"maxUsers and maxSsoUsers are mutually exclusive; send only maxUsers",
)
ErrConflictingUserLimitKeys is returned when a payload sends both the canonical maxUsers key and its deprecated maxSsoUsers alias at once.
var ErrDeliveryDetailsScanType = errors.New("unsupported type for DeliveryDetails.Scan")
ErrDeliveryDetailsScanType is returned when DeliveryDetails.Scan receives a value of an unexpected SQL type.
var ErrInvalidCapabilitySet = errors.New("invalid capability set")
ErrInvalidCapabilitySet is returned by ValidateCapabilitySet.
var ErrStatusPageSettingsScanType = errors.New("unsupported type for StatusPageSettings.Scan")
ErrStatusPageSettingsScanType is returned when StatusPageSettings.Scan receives a value of an unexpected SQL type.
var ErrUnknownEntitlementsPayloadVersion = errors.New("unknown entitlements payload version")
ErrUnknownEntitlementsPayloadVersion is returned when the payload JSON's version discriminator does not match a known shape. Callers can use errors.Is to detect it.
Functions ¶
func AbandonedResultThreshold ¶
AbandonedResultThreshold returns how old a lifecycle-marker raw row must be, relative to its check's period, before the abandoned-result reaper may finalize it: AbandonedResultMultiplier * (period + AbandonedResultLeaseGrace). This is "the check's period plus the worker lease timeout, with a generous multiplier" from spec 2026-08-18-03's Proposal.
func CapBody ¶
CapBody truncates a body to DeliveryDetailsBodyCap bytes, appending a marker when truncation occurred so the operator knows the stored copy is partial.
func ContactRequiresVerification ¶
ContactRequiresVerification reports whether a contact type must complete the verification code round-trip before it can be used.
func EventTypeLikePattern ¶
EventTypeLikePattern turns a family prefix ("auth", or "auth." — both are accepted) into the SQL LIKE pattern matching every type in that family.
The escaping is not paranoia: real family names contain `_`, which LIKE treats as "any single character". Without it, a filter on "oncall_schedule" would also admit a hypothetical "oncallXschedule.*" family, and the *exclusion* used to hide auth events from non-admins would be the dangerous direction of the same bug.
func HideBrandingSectionPatch ¶
HideBrandingSectionPatch returns the `branding`-rooted merge patch that flips ONLY the white-label opt-in, leaving the two asset keys alone — what Postgres concatenates onto `settings->'branding'`. It is the generic PATCH path's write (StatusPageUpdate.HideBranding).
func HideBrandingSettingsPatch ¶
HideBrandingSettingsPatch returns the same patch rooted at `settings` — what SQLite hands to json_patch.
func IsActiveAt ¶
func IsActiveAt(window *MaintenanceWindow, target time.Time) bool
IsActiveAt determines whether a maintenance window is active at the given time.
func IsValidReportFrequency ¶
IsValidReportFrequency reports whether a frequency string is accepted.
func LikeContainsPattern ¶
LikeContainsPattern builds the SQL LIKE pattern matching any value that CONTAINS the given text, escaping the LIKE metacharacters so a user typing "100%" or "check_1" searches for those characters rather than for a wildcard. Pair it with EventTypeLikeEscape.
func MigrateRegionList ¶
MigrateRegionList rewrites a check's region list, replacing every occurrence of `from` with `to` while preserving order and de-duplicating: a check that already declares BOTH slugs (the half-migrated case) must not end up with `to` twice, because the resulting check_jobs would violate the unique (check_uid, region) index.
The second return reports whether anything actually changed, so a caller can skip a no-op UPDATE — which is what makes the region migration idempotent.
Pure and allocation-light on purpose: it lives on the model package so both SQL dialects can share one definition of "what the rewritten array is", rather than expressing it twice in dialect-specific SQL.
func RawAvailability ¶
RawAvailability computes (successCount, countableTotal) over raw results, skipping lifecycle markers and reaped/abandoned attempts. Callers derive pct = 100*success/total when total > 0.
func ResultColumnsWithoutBlobs ¶
ResultColumnsWithoutBlobs returns every persisted column of Result except the two blobs, in struct-field order, qualified with the given table alias ("" for none).
It is derived from the model's own bun tags rather than transcribed, so a column added to Result reaches this projection automatically instead of silently scanning as a zero value. TestResultColumnsWithoutBlobs pins that.
func RollupPageStatus ¶
func RollupPageStatus(resources []PageResourceStatus) (PageStatus, PageStatusCounts)
RollupPageStatus computes a status page's overall status from the live status of every resource across every section of the page. It is pure and exported — no DB access, no request context — so spec 2026-08-08-06 (the public summary endpoint) and spec 2026-08-08-07 (the page-level SVG badge) can call it directly against the same resource list ViewStatusPage builds, without reimplementing the aggregation.
Rules, evaluated in priority order over the whole resource list:
- Resources currently in maintenance are excluded from the outage scan — maintenance masks failures, so a resource that goes down *during* its own maintenance window never taints the page.
- Else any non-maintenance resource with CheckStatusDown -> PageStatusDown.
- Else any resource with CheckStatusDegraded or CheckStatusWarning -> PageStatusDegraded.
- Else if any resource is in maintenance -> PageStatusMaintenance.
- Else if no resource has a usable status (all CheckStatusCreated / no resources at all) -> PageStatusUnknown.
- Else -> PageStatusOperational.
func StateKey ¶
StateKey joins parts with colons to create namespaced keys. Example: StateKey("incident", incidentUID, "slack_notification") returns "incident:abc123:slack_notification".
func Status ¶
func Status(window *MaintenanceWindow, now time.Time) string
Status returns "active", "upcoming", or "past" for the window at now.
It is the canonical recurrence-status semantics shared with the frontend (computeMaintenanceStatus): active if IsActiveAt; else past for a non-recurring window after its start, or a recurring window after its RecurrenceEnd; else upcoming.
func StatusToString ¶
StatusToString converts a ResultStatus integer to its string representation.
func TruncateSupportBody ¶
TruncateSupportBody caps a body at SupportBodyMaxLength, reporting whether it had to cut. Rune-aware, so a cap never splits a multi-byte character.
func ValidCustomDomainState ¶
ValidCustomDomainState reports whether s is one of the lifecycle states.
func ValidStatusPageVisibility ¶
ValidStatusPageVisibility reports whether v is one of the three supported visibility values.
func ValidSupportChannel ¶
ValidSupportChannel reports whether a channel value is one we accept.
func ValidSupportStatus ¶
ValidSupportStatus reports whether a status value is one we accept.
func ValidateCapabilitySet ¶
ValidateCapabilitySet reports whether a reported set is storable: every name a lowercase `[a-z0-9-]+` slug, and no duplicates. It is the Go mirror of the database CHECK constraint (Postgres) and triggers (SQLite).
IT EXISTS TO PROTECT LIVENESS, NOT TO REPLACE THE DATABASE. A capability set arrives from a remote agent, so it is untrusted input on a code path that also refreshes last_active_at. Letting a malformed array fail the whole UPDATE would take the region's liveness down with it — the agent would still be running checks while its region quietly went stale. Callers reject the set (falling back to "not reported", which changes nothing) and still write the heartbeat. The database remains the authority that garbage is never stored.
A nil set is valid: it means "not reported".
func VerifiableContactTypes ¶
VerifiableContactTypes are the contact types that require a code round-trip before they may be paged. Email and web push are self-verifying (delivery respectively subscription is the proof), so they are absent here.
Types ¶
type ActorType ¶
type ActorType string
ActorType represents who triggered an event.
const ( // ActorTypeSystem indicates the event was triggered by the system. ActorTypeSystem ActorType = "system" // ActorTypeUser indicates the event was triggered by a user. ActorTypeUser ActorType = "user" // ActorTypeAPIToken indicates the event was triggered through a personal // access token or agent key rather than an interactive session. ActorUID // still names the owning user when one is known. ActorTypeAPIToken ActorType = "api_token" // ActorTypeService indicates the event was triggered by another trusted // service (a signed service-to-service call, e.g. the billing service). ActorTypeService ActorType = "service" )
type Agent ¶
type Agent struct {
UID string `bun:"uid,pk,type:varchar(36)"`
// OrganizationUID is nil exactly for system agents.
OrganizationUID *string `bun:"organization_uid"`
// Kind is AgentKindOrg or AgentKindSystem.
Kind string `bun:"kind,notnull"`
// Region is the region slug the agent is bound to; all its claims are
// hard-scoped to it. Fully-qualified `@<org>/<region>` for an org agent, a
// plain cloud region slug for a system agent.
Region string `bun:"region,notnull"`
Name string `bun:"name,notnull"`
// Ed25519PublicKey is the base64 identity public key used to verify reconnect
// signatures.
Ed25519PublicKey string `bun:"ed25519_public_key,notnull"`
// X25519PublicKey is the age recipient string ("age1…") credentials are
// sealed to.
X25519PublicKey string `bun:"x25519_public_key,notnull"`
// Fingerprint is a short hash of the Ed25519 key, shown in UI/logs.
Fingerprint string `bun:"fingerprint,notnull"`
Status string `bun:"status,notnull"`
LastSeenAt *time.Time `bun:"last_seen_at"`
EnrolledAt time.Time `bun:"enrolled_at,notnull,default:current_timestamp"`
RevokedAt *time.Time `bun:"revoked_at"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
Agent is a deported check agent that connects outbound-only over WebSocket, is hard-scoped to exactly one region, and can do nothing but claim/execute/submit checks. The DB never holds a usable agent credential — only the agent's public keys.
Kind decides the scope: an `org` agent is bound to one organization and one private region; a `system` agent has no organization at all and serves one shared cloud region for every org. The DB enforces the pairing (kind = 'org' <=> organization_uid is not null).
func NewSystemAgent ¶
NewSystemAgent builds an active platform-operated agent row. It carries no organization: its claims are scoped by the shared cloud region alone.
type AgentEnrollmentToken ¶
type AgentEnrollmentToken struct {
UID string `bun:"uid,pk,type:varchar(36)"`
// OrganizationUID is nil exactly for system tokens.
OrganizationUID *string `bun:"organization_uid"`
// Kind is AgentKindOrg or AgentKindSystem.
Kind string `bun:"kind,notnull"`
// Region is the region the enrolled agent will be bound to:
// `@<org>/<region>` for an org token, a cloud region slug for a system one.
Region string `bun:"region,notnull"`
// TokenHash is the SHA-256 hex of the spe_ token — never the token itself.
TokenHash string `bun:"token_hash,notnull"`
ExpiresAt time.Time `bun:"expires_at,notnull"`
// MaxUses bounds a system token's enrollments (nil = unlimited). Never set
// on an org token, which is one-shot by construction.
MaxUses *int `bun:"max_uses"`
// UseCount is how many agents enrolled with this token (0 or 1 for org).
UseCount int `bun:"use_count,notnull"`
// UsedAt / UsedByAgentUID record the LAST enrollment. For an org token they
// are also the single-use marker.
UsedAt *time.Time `bun:"used_at"`
UsedByAgentUID *string `bun:"used_by_agent_uid"`
CreatedByUserUID *string `bun:"created_by_user_uid"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
AgentEnrollmentToken binds a future agent to a region. Only the SHA-256 hash of the token is stored; the token itself is readable exactly once.
An `org` token is minted by an org admin, bound to (org, private region), and strictly ONE-SHOT: enrollment atomically marks it used while creating the agent row. A `system` token is platform-operator material seeded from SP_SYSTEM_AGENT_ENROLLMENT_TOKENS, bound to a cloud region slug and to no org, and MULTI-USE (bounded by MaxUses, nil = unlimited): every machine of a fly fleet generates its own keypair and enrolls on boot, so no private key is ever shared between machines.
func NewAgentEnrollmentToken ¶
func NewAgentEnrollmentToken( orgUID, region, tokenHash string, expiresAt time.Time, createdByUserUID *string, ) *AgentEnrollmentToken
NewAgentEnrollmentToken builds a one-shot org enrollment token row bound to (org, private region) with the given hash and expiry.
func NewSystemAgentEnrollmentToken ¶
func NewSystemAgentEnrollmentToken( region, tokenHash string, expiresAt time.Time, maxUses *int, ) *AgentEnrollmentToken
NewSystemAgentEnrollmentToken builds a multi-use platform enrollment token bound to a cloud region slug (no owning org). maxUses nil = unlimited.
func (*AgentEnrollmentToken) HasUsesLeft ¶
func (t *AgentEnrollmentToken) HasUsesLeft() bool
HasUsesLeft reports whether the token may still enroll an agent. Org tokens are one-shot (the atomic consume is the real guard); system tokens are unlimited unless MaxUses is set.
func (*AgentEnrollmentToken) IsSystem ¶
func (t *AgentEnrollmentToken) IsSystem() bool
IsSystem reports whether this is a platform (multi-use) enrollment token.
func (*AgentEnrollmentToken) OrgUID ¶
func (t *AgentEnrollmentToken) OrgUID() string
OrgUID returns the owning organization UID, or "" for a system token.
type AgentNonce ¶
type AgentNonce struct {
AgentUID string `bun:"agent_uid,pk"`
Nonce string `bun:"nonce,pk"`
SeenAt time.Time `bun:"seen_at,notnull"`
}
AgentNonce is one consumed reconnect nonce. The table is the cluster-wide replay guard: the per-process cache it replaces was only sound with a single API replica, which a multi-machine fly fleet reconnecting through a load balancer is not.
type AggregateResultsFunc ¶
AggregateResultsFunc computes the single rollup row for one bucket from its source rows and returns the UIDs of the source rows to delete. It runs inside CompactResults' transaction and must be pure (no DB access). Returning a nil rollup or an empty sourceUIDs signals "nothing measurable to compact" — the sources are left in place and the transaction commits without writing.
type AppSetting ¶
type AppSetting struct {
Key string `bun:"key,pk,type:text"`
Value string `bun:"value,notnull,type:text"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}
AppSetting is a generic key/value store for server-level configuration that must survive restarts (e.g., VAPID keys for Web Push).
type AutoResolvePolicy ¶
type AutoResolvePolicy string
AutoResolvePolicy decides what an auto-created publication does when its backing incident resolves.
const ( // AutoResolveAlways resolves the publication regardless of human edits. AutoResolveAlways AutoResolvePolicy = "always" // AutoResolveIfUntouched (default) resolves only while nobody has edited // the publication; once a human owns the narrative, the automation posts a // "component recovered" monitoring note and leaves the final resolve to // them. It applies to every publication LINKED to the resolving incident, // hand-published ones included — only free-form entries are out of scope. AutoResolveIfUntouched AutoResolvePolicy = "if_untouched" // AutoResolveNever posts nothing and resolves nothing. AutoResolveNever AutoResolvePolicy = "never" )
AutoResolvePolicy values.
func (AutoResolvePolicy) IsValid ¶
func (p AutoResolvePolicy) IsValid() bool
IsValid reports whether the policy is one of the three recognized values.
type AvailabilityCounts ¶
AvailabilityCounts is a (success, countable-total) tally over some window, potentially folded together from more than one period_type tier (e.g. an org's trailing-24h availability combines `hour` rollups and `raw` rows — see checks.Service.GetCheckStats / spec 2026-08-26-09). Callers derive the percentage via Pct rather than dividing directly, so "no measurable data" (Total == 0) can never be silently rendered as a manufactured 100%.
func (AvailabilityCounts) Pct ¶
func (c AvailabilityCounts) Pct() (float64, bool)
Pct returns 100*Success/Total and ok=true when Total > 0. ok=false means the window had no countable data at all — the caller must render that as "no data", never as a fabricated percentage.
type AvailabilitySettings ¶
type AvailabilitySettings struct {
// ThresholdUp is the green floor (pct >= this renders "up"). nil = 99.9.
ThresholdUp *float64 `json:"thresholdUp,omitempty"`
// ThresholdDegraded is the amber floor (pct >= this and < ThresholdUp
// renders "degraded"; below it renders "down", subject to the
// small-bucket calibration guard). nil = 99.0.
ThresholdDegraded *float64 `json:"thresholdDegraded,omitempty"`
}
AvailabilitySettings customizes the green/amber/red availability thresholds rendered on a status page's bars. Nil fields fall back to the package defaults (DefaultAvailabilityThresholdUp/Degraded).
func (*AvailabilitySettings) EffectiveThresholds ¶
func (a *AvailabilitySettings) EffectiveThresholds() (float64, float64)
EffectiveThresholds resolves the up/degraded thresholds for a (possibly nil) AvailabilitySettings, falling back to the package defaults for a nil receiver or nil fields. Never returns a value that needs further nil checking by the caller.
type BrandingSettings ¶
type BrandingSettings struct {
// LogoFileUID is the `files.uid` of the page's own logo. nil = wear the
// SolidPing logo.
LogoFileUID *string `json:"logoFileUid,omitempty"`
// FaviconFileUID is the `files.uid` of the page's own favicon. nil = the
// default favicon.
FaviconFileUID *string `json:"faviconFileUid,omitempty"`
// HideBranding is the page's HALF of the white-label decision: the
// "powered by SolidPing" footer disappears only when this is true AND the
// org holds the `whiteLabel` entitlement. Keeping the two halves separate
// means a downgrade silently restores the badge without rewriting the page.
HideBranding bool `json:"hideBranding,omitempty"`
}
BrandingSettings is the page's brand identity. File UIDs, not URLs — the public URL is derived (statuspageassets.PublicURL), so the stored value stays valid when the route changes.
type Capabilities ¶
type Capabilities struct {
// CanNotify reports whether the integration can receive outbound
// notifications (i.e. it can act as a "channel" / notification target).
CanNotify bool
// CanSource reports whether the integration provides data that checks
// read from (e.g. the Freebox line-quality source).
CanSource bool
// CanSendSMS / CanPlaceCall report the two PHONE capabilities, resolved
// independently of each other. They are separate because the providers
// are: OVHcloud sells SMS but has no voice API, so an instance can run OVH
// for SMS and Twilio for voice at the same time. Binding them would mean
// giving up escalation calls in exchange for cheaper SMS.
CanSendSMS bool
CanPlaceCall bool
}
Capabilities describes what roles an integration type can play. The two flags are independent: a single type may be both a notification sink and a data source. They replace the former hard-coded "Freebox is a source, not a sink" carve-out in notifications.GetSender — capability is now data, not a special case.
func CapabilitiesFor ¶
func CapabilitiesFor(t ConnectionType) Capabilities
CapabilitiesFor returns the capabilities of an integration connection type. Every notification sink (slack, discord, webhook, email, googlechat, mattermost, msteams, ntfy, gotify, zulip, pagerduty, pushover) is CanNotify; freebox is a data source (CanSource) and cannot receive notifications. Twilio additionally carries the two phone capabilities — it is the only connection type that is a bring-your-own SMS *and* voice account. The default branch intentionally covers every current notification-sink type, so only data sources and the phone type need an explicit case.
type CapabilityState ¶
type CapabilityState uint8
CapabilityState is the THREE-state answer to "does this worker have X?". A distinct type rather than a bool, so a caller physically cannot collapse "unknown" into "no".
const ( // CapabilityStateUnknown means the worker has never reported its set. CapabilityStateUnknown CapabilityState = iota // CapabilityStateAbsent means the worker reported, and this capability was // not in the set. That is a real "no", not an absence of information. CapabilityStateAbsent // CapabilityStatePresent means the worker reported this capability. CapabilityStatePresent )
type Check ¶
type Check struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
CheckGroupUID *string `bun:"check_group_uid"`
Name *string `bun:"name"`
Slug *string `bun:"slug"`
Description *string `bun:"description"`
Type string `bun:"type,notnull"`
Config JSONMap `bun:"config,type:jsonb,nullzero"`
// ConfigPrivate holds the AES-GCM envelope (JSON) for the secret keys
// split out of Config at write time. NULL when no encrypted secrets exist
// on this row — distinct from "encryption disabled at the server".
ConfigPrivate *string `bun:"config_private,type:text,nullzero"`
// ConfigPrivateKeys is a JSON array of the key names (e.g. `["password"]`)
// whose values live in ConfigPrivate. Non-secret by construction; surfaced
// to the dashboard so it can render placeholder hints without decrypting.
ConfigPrivateKeys *string `bun:"config_private_keys,type:text,nullzero"`
// ConfigSealed holds the region-sealed (age X25519, v2) envelope of the same
// secret keys when the check targets one or more org-private regions (spec
// 2026-07-16-02): sealed to the X25519 keys of the region's active agents.
// A check targeting ONLY private regions stores secrets sealed-only
// (ConfigPrivate stays NULL — the server cannot decrypt them after write);
// a mixed private+cloud check dual-stores (v1 envelope for cloud dispatch +
// this sealed blob for agents).
ConfigSealed *string `bun:"config_sealed,type:text,nullzero"`
Regions []string `bun:"regions,type:text[],array"`
Enabled bool `bun:"enabled,notnull"`
Internal bool `bun:"internal,notnull"`
Period timeutils.Duration `bun:"period,notnull"`
// CreatedBy is the users.uid of whoever created this check, or NULL when
// nobody did — the startup job's seeded samples, and every check that
// predates the column (spec 2026-09-06-02). It is recorded for EVERY
// creator, not only demo sessions: "who made this" is useful audit data in
// its own right, and a column populated on one code path only is a column
// nobody can trust.
//
// Deliberately not a foreign key: a check outlives the account that made
// it, and users are soft-deleted. This is a historical attribution.
//
// It is also what makes seeded demo checks immutable to a demo session
// without any "protected" flag: the ownership rule is
// `created_by == claims.UserUID`, and NULL never equals a UID.
CreatedBy *string `bun:"created_by,nullzero"`
// RegionSpread is the optional inter-region scheduling offset ("spread")
// applied between consecutive regions' phases (spec 2026-07-20-05). NULL =
// the default of Period / region_count (even coverage across the period);
// a non-null value forces a fixed offset (e.g. 0 = all regions fire
// together for comparative cross-region sampling), validated
// 0 <= RegionSpread < Period. It is a first-class scheduling input (it
// drives check_jobs phase), not checker config, hence a column like Period.
RegionSpread *timeutils.Duration `bun:"region_spread,nullzero"`
// Incident tracking — wall-clock periods (seconds). Replaces the old
// count-based thresholds per spec
// 2026-05-08-02-time-based-confirmation-and-recovery-periods.md.
// `0` means "open / resolve immediately on the first opposite signal".
ConfirmationPeriodSeconds int `bun:"confirmation_period_seconds,notnull"`
RecoveryPeriodSeconds int `bun:"recovery_period_seconds,notnull"`
// EscalationThreshold remains streak-based for now — it gates the *second*
// notification step, not the incident open. Will be re-modeled when the
// escalation-severity primitive ships. No `default:` clause even though
// the column has one — see the StatusPage.AutoPublishDelaySeconds note:
// `default:3` made `escalation_threshold: 0` unwritable on create.
EscalationThreshold int `bun:"escalation_threshold,notnull"`
// FirstFailureAt is set on the result that flips the streak from 0 to 1
// on a failing check (no active incident yet). Cleared on the next success.
// The incident opens when now - FirstFailureAt >= ConfirmationPeriod.
FirstFailureAt *time.Time `bun:"first_failure_at"`
// FirstSuccessSinceFailureAt is set on the first success arriving while
// an incident is open. Cleared by any subsequent failure during the
// recovery window. Auto-resolve fires when
// now - FirstSuccessSinceFailureAt >= RecoveryPeriod.
FirstSuccessSinceFailureAt *time.Time `bun:"first_success_since_failure_at"`
// Adaptive resolution settings.
//
// ReopenCooldownMultiplier (nil = code default) drives the short
// blip-dedup window: a fast relapse reattaches to the just-resolved
// incident instead of paging again. Independent of the flapping layer.
ReopenCooldownMultiplier *int `bun:"reopen_cooldown_multiplier"`
// Flapping (adaptive recovery) config — spec 2026-06-30-07. When a check
// flaps (repeated outages over a short horizon) the required stability
// before auto-resolving grows per flap, bounded by a cap. Off-by-default-
// equivalent: FlapBackoffFactor==1 or FlappingWindowSeconds==0 reproduces
// the constant RecoveryPeriodSeconds behavior.
//
// Which is precisely why none of the three carries a `default:` clause,
// even though all three columns have one — see the
// StatusPage.AutoPublishDelaySeconds note. With `default:21600` on the tag,
// `flappingWindowSeconds: 0` never reached the database, so flapping could
// not be turned off at creation time (spec 2026-08-30-04). NewCheck
// supplies the 21600/2/8 defaults instead.
FlappingWindowSeconds int `bun:"flapping_window_seconds,notnull"`
FlapBackoffFactor int `bun:"flap_backoff_factor,notnull"`
MaxRecoveryMultiplier int `bun:"max_recovery_multiplier,notnull"`
// Flap state, updated only on the rare incident-open/reopen (never per
// result). FlapCount is the number of outages accumulated inside the
// rolling flapping window; LastOutageAt is the wall-clock of the most
// recent outage onset and gates the window reset.
FlapCount int `bun:"flap_count,notnull"`
LastOutageAt *time.Time `bun:"last_outage_at"`
// Optional escalation policy. Falls back to the check_group's policy
// (and ultimately to no escalation) when nil.
EscalationPolicyUID *string `bun:"escalation_policy_uid"`
// TracerouteOnFailure is the per-check override for the MTR-style path
// capture taken when this check goes down on a network-reachability
// failure (spec 2026-08-21-10).
//
// THREE STATES, AND nil IS THE INTERESTING ONE:
//
// nil inherit the org default (org parameter
// `diagnostics.traceroute.enabled`, itself ON per the spec)
// &true always trace this check
// &false never trace this check, whatever the org default says
//
// A plain bool would collapse "not decided" into "no", which would make
// the org-level default unreachable for every check that already exists.
TracerouteOnFailure *bool `bun:"traceroute_on_failure"`
// Status tracking
Status CheckStatus `bun:"status,notnull"`
StatusStreak int `bun:"status_streak,notnull"`
StatusChangedAt *time.Time `bun:"status_changed_at"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
// GroupSortKey is the effective group-ordering key populated only by the
// sort=group ListChecks path: the check's group sort_order, or a large
// sentinel (int16 max is 32767, so ungrouped sorts strictly last). Scan-only
// and transient — never selected, inserted, or updated outside that query.
GroupSortKey int64 `bun:"group_sort_key,scanonly"`
// TargetHostSortKey is the effective sort=targetHost ordering key: the
// check's config host/url/target text (best-effort, not hostname-parsed —
// see targetHostSortKeyExpr), or a sentinel that sorts strictly last for
// checks with none of those fields. Scan-only and transient; distinct from
// the response's TargetHost (checkerdef.ExtractTargetHost), which is the
// precise, hostname-parsed value clients bucket by.
TargetHostSortKey string `bun:"target_host_sort_key,scanonly"`
}
Check represents a monitoring configuration.
func (*Check) EffectiveFlapCount ¶
EffectiveFlapCount returns the number of outages counted inside the current rolling flapping window, as of `now`.
THE LAZY-RESET TRAP: FlapCount (the raw column) only resets to 0 at the NEXT outage onset (see incidents.bumpFlap) — a check whose last outage was e.g. 12h ago can still hold a stale nonzero FlapCount in the row, because nothing has come along yet to reset it. Any caller that reads FlapCount directly to describe the check's CURRENT state (rather than to drive the active incident's own recovery math, where it is always fresh) must use this method instead, or it will report a flap level that stopped being true hours or days ago.
func (*Check) EffectiveRecoveryPeriod ¶
EffectiveRecoveryPeriod returns the stability required before an incident on this check auto-resolves, given the check's RAW (possibly stale) FlapCount. This is what the incidents package uses while an incident is active: FlapCount was just written by bumpFlap at this incident's own onset, so it is always fresh in that context — the lazy-reset trap does not apply here. See effectiveRecoveryPeriodForFlapCount for the math.
func (*Check) EffectiveRecoveryPeriodAt ¶
EffectiveRecoveryPeriodAt returns the same computation as EffectiveRecoveryPeriod, but driven by EffectiveFlapCount(now) rather than the raw column — i.e. it is lazy-reset aware. Use this to describe a check's CURRENT adaptive-recovery state from outside an active incident (e.g. the API's flapState block), where the raw FlapCount may be stale.
func (*Check) FlappingWindowElapsed ¶
FlappingWindowElapsed reports whether the rolling flapping window has elapsed as of `now`, i.e. whether the NEXT outage onset would start a fresh window rather than count as a flap inside the current one. True when there has been no outage yet (LastOutageAt nil), the flapping feature is off (FlappingWindowSeconds == 0), or the last outage is older than the window.
This is the one place the "lazy reset" rule is expressed — both the write-path counter bump (incidents.bumpFlap, via this method) and the read-path effective-value exposure (EffectiveFlapCount) delegate to it, so the two can never drift apart.
func (*Check) RegionSpreadDuration ¶
RegionSpreadDuration returns the check's optional inter-region spread override as a *time.Duration (nil when unset), for the scheduling.RegionSpread resolver. Keeps the *timeutils.Duration ⇄ *time.Duration conversion in one place so the reconcile, create, and worker paths all resolve the identical spread.
func (*Check) TimeoutOrDefault ¶
TimeoutOrDefault resolves the check's per-execution timeout: the explicit `timeout` entry in its config when it parses to a positive duration, and `defaultTimeout` (the server's scheduling.check_timeout_ms) otherwise.
This is a READ-side approximation of what the worker actually applies — the worker additionally clamps an unset timeout by the cost EWMA and caps an explicit one (checkworker.perCheckTimeout). Both consumers here want an upper bound on "how late can this check possibly notice an outage", and the unclamped default is exactly that bound: the cost-aware clamp only ever shortens it.
type CheckConnection ¶
type CheckConnection struct {
bun.BaseModel `bun:"table:check_channels,alias:check_channel"`
UID string `bun:"uid,pk,type:varchar(36)"`
CheckUID string `bun:"check_uid,notnull"`
ConnectionUID string `bun:"integration_uid,notnull"`
OrganizationUID string `bun:"organization_uid,notnull"`
Settings *JSONMap `bun:"settings,type:jsonb"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
// Relations (optional, for eager loading)
Check *Check `bun:"rel:belongs-to,join:check_uid=uid"`
Connection *Integration `bun:"rel:belongs-to,join:integration_uid=uid"`
Organization *Organization `bun:"rel:belongs-to,join:organization_uid=uid"`
}
CheckConnection represents the many-to-many relationship between checks and the integrations they notify through. Backed by the `check_channels` table (the binding keeps the "channel" name to match the notify-role taxonomy); the FK column is `integration_uid`. The Go field name ConnectionUID is retained.
func NewCheckConnection ¶
func NewCheckConnection(checkUID, connectionUID, organizationUID string) *CheckConnection
NewCheckConnection creates a new check-connection relationship with generated UID.
type CheckConnectionUpdate ¶
type CheckConnectionUpdate struct {
Settings *JSONMap
}
CheckConnectionUpdate represents fields that can be updated for a check-connection.
type CheckDependency ¶
type CheckDependency struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
ParentCheckUID string `bun:"parent_check_uid,notnull"`
ChildCheckUID string `bun:"child_check_uid,notnull"`
Kind CheckDependencyKind `bun:"kind,notnull"`
Description *string `bun:"description"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
CheckDependency is a directed edge (parent → child) inside an org's dependency DAG. The same org constraint is enforced at write time.
func NewCheckDependency ¶
func NewCheckDependency( orgUID, parentUID, childUID string, kind CheckDependencyKind, description *string, ) *CheckDependency
NewCheckDependency builds a fresh edge.
type CheckDependencyKind ¶
type CheckDependencyKind string
CheckDependencyKind enumerates whether a parent failure is a hard or soft cause for the child. Hard edges drive rollup (paging suppression); soft edges are informational only.
const ( // CheckDependencyKindHard means the child reliably fails when the parent does. CheckDependencyKindHard CheckDependencyKind = "hard" // CheckDependencyKindSoft means the child may degrade but won't necessarily fail. CheckDependencyKindSoft CheckDependencyKind = "soft" )
func (CheckDependencyKind) IsValid ¶
func (k CheckDependencyKind) IsValid() bool
IsValid reports whether the value is one of the known kinds.
type CheckDependencyUpdate ¶
type CheckDependencyUpdate struct {
Kind *CheckDependencyKind
Description *string
ClearDescription bool
}
CheckDependencyUpdate captures the writable fields. Pointer = optional.
type CheckGroup ¶
type CheckGroup struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
Name string `bun:"name,notnull"`
Slug string `bun:"slug,notnull"`
Description *string `bun:"description"`
SortOrder int16 `bun:"sort_order,notnull"`
// Optional escalation policy. NULL = no group-level policy.
EscalationPolicyUID *string `bun:"escalation_policy_uid"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
// Computed field (not stored in DB)
CheckCount int `bun:"check_count,scanonly"`
}
CheckGroup represents a flat organizational group for checks.
func NewCheckGroup ¶
func NewCheckGroup(orgUID, name, slug string) *CheckGroup
NewCheckGroup creates a new check group with generated UID.
type CheckGroupUpdate ¶
type CheckGroupUpdate struct {
Name *string
Slug *string
Description *string
SortOrder *int16
EscalationPolicyUID *string
ClearEscalationPolicyUID bool
}
CheckGroupUpdate represents fields that can be updated on a check group.
type CheckJob ¶
type CheckJob struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
CheckUID string `bun:"check_uid"`
Region *string `bun:"region"`
Type string `bun:"type"`
Config JSONMap `bun:"config,type:jsonb,nullzero"`
// ConfigPrivate / ConfigPrivateKeys mirror the columns on Check — the
// scheduler copies them when materializing a job so workers never see
// plaintext secrets in the row at rest.
ConfigPrivate *string `bun:"config_private,type:text,nullzero"`
ConfigPrivateKeys *string `bun:"config_private_keys,type:text,nullzero"`
// ConfigSealed mirrors Check.ConfigSealed: the region-sealed (age X25519)
// envelope shipped VERBATIM to deported agents — the server never decrypts
// it on the agent dispatch path (spec 2026-07-16-02).
ConfigSealed *string `bun:"config_sealed,type:text,nullzero"`
Encrypted bool `bun:"encrypted,notnull"`
Period timeutils.Duration `bun:"period,notnull"`
ScheduledAt *time.Time `bun:"scheduled_at"`
LeaseWorkerUID *string `bun:"lease_worker_uid"`
LeaseExpiresAt *time.Time `bun:"lease_expires_at"`
LeaseStarts int `bun:"lease_starts,notnull"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
// Cost-aware, plan-weighted scheduling (spec 2026-06-30-09).
//
// CostEWMAMs is an exponentially-weighted moving average of execution
// duration in milliseconds, updated in the post-exec write (ReleaseLease).
// Timeouts pin it to the ceiling. Drives slow-lane classification and the
// cost-aware execution timeout. 0 until the job's first run.
CostEWMAMs float64 `bun:"cost_ewma_ms,notnull"`
// DelayEWMAMs is an EWMA of how late the job actually started relative to
// its real scheduled_at (probe start − scheduled_at, floored at 0), updated
// in the post-exec write. Pure telemetry (spec 2026-07-01-02): it feeds the
// cost-distribution endpoint and the lane-split go/no-go but never the claim
// order — delay is a victim signal, and folding it into the offset punished
// starved checks and spiraled unboundedly under overload. 0 until the job's
// first run. Added by migration 007.
DelayEWMAMs float64 `bun:"delay_ewma_ms,notnull"`
// PlanWeight is the denormalized plan tier copied from org_entitlements
// (0 = free; higher = more protected). Reserved capacity + deadline credit
// for paid orgs. Refreshed on entitlement change and reconcile.
PlanWeight int `bun:"plan_weight,notnull"`
// EffectiveScheduledAt is scheduled_at + cost_ewma×CostOffsetWeight −
// tier_credit, with the offset clamped to scheduling.MaxDeprioritizeOffset.
// The claim SELECT gates on scheduled_at but orders by this column, so
// de-prioritization only bites under contention (D2/Option A). Backfilled to
// scheduled_at by migration 006; delay-era offsets healed by migration 008.
//
// A rate-limited deferral deliberately does NOT re-anchor this column
// (checkjobsvc.DeferLeaseRateLimited, spec 2026-08-26-02): a job the per-org
// bucket turned away keeps the tick it missed as its ordering key, so it
// grows more overdue every window it loses and wins the next contended slot.
// That is what makes an over-cap org rotate its deficit instead of starving
// the same UID-hash phases forever.
EffectiveScheduledAt *time.Time `bun:"effective_scheduled_at"`
// Lane is the scheduling class (spec 2026-07-01-03): 0 = fast, 1 = slow
// (scheduling.LaneFast / LaneSlow). Classified from the cost EWMA with
// hysteresis in the post-exec write; new rows start fast (first run is
// FIFO; one execution reclassifies). The claim runs two lane-filtered
// SELECTs so slow jobs can never occupy more than pool_size −
// fast_lane_reserved slots on a worker. Added by migration 009.
Lane uint8 `bun:"lane,notnull"`
// Check is the check this job executes, populated at claim time by
// ClaimJobs / ClaimJobsForCheck so the incident hot path can skip a
// per-result GetCheck round-trip. Transient: never persisted (bun:"-").
// May be nil if the check was deleted between scheduling and claim.
Check *Check `bun:"-"`
}
CheckJob represents a scheduled job for executing a check.
func NewCheckJob ¶
NewCheckJob creates a new check job with generated UID.
func (*CheckJob) IsInternal ¶
IsInternal reports whether this job belongs to an internal, server-created check (worker self-stats plumbing).
ONE HELPER SO THE TWO RATE GATES CANNOT DISAGREE — the in-process worker gate (checkworker.applyRateLimitGate) and the agent dispatch gate (agentws.handleClaim) both call it, the same way both delegate "what is a passive check" to checkerdef. An internal check is exempt from the MaxChecks quota and invisible to the checks-per-minute demand figure, so it must not draw MaxChecksPerMinute tokens either (spec 2026-08-27-01): counting nowhere has to mean counting nowhere, or the predicted demand and the factual skip counter describe different fleets.
Reads the check attached at claim time (checkjobsvc.attachChecks, inside the claim transaction) — no column on check_jobs, no migration. A nil Check means the row was deleted between scheduling and claim; that job is treated as a normal metered one, which is the safe direction.
type CheckJobUpdate ¶
type CheckJobUpdate struct {
Region *string
Config *JSONMap
ConfigPrivate *string
ConfigPrivateKeys *string
Encrypted *bool
Period *timeutils.Duration
ScheduledAt *time.Time
LeaseWorkerUID *string
LeaseExpiresAt *time.Time
LeaseStarts *int
}
CheckJobUpdate represents fields that can be updated.
type CheckLabel ¶
type CheckLabel struct {
UID string `bun:"uid,pk,type:varchar(36)"`
CheckUID string `bun:"check_uid,notnull"`
LabelUID string `bun:"label_uid,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
}
CheckLabel represents the many-to-many relationship between checks and labels.
func NewCheckLabel ¶
func NewCheckLabel(checkUID, labelUID string) *CheckLabel
NewCheckLabel creates a new check-label relationship with generated UID.
type CheckRate ¶
type CheckRate struct {
// UID identifies the row so a caller can project a hypothetical change:
// drop the check being edited out of the sum and add its proposed shape
// back (spec 2026-08-26-05's validate-time rate warning).
UID string `bun:"uid"`
Enabled bool `bun:"enabled"`
Period timeutils.Duration `bun:"period"`
Regions []string `bun:"regions,type:text[],array"`
Type string `bun:"type"`
}
CheckRate is a thin projection of a check used to compute usage stats: whether the check is enabled, its execution period, and its region set. Returned by ListOrgCheckRates so the entitlements service can sum the aggregate checks-per-minute in Go (the SQL interval/text representation of Period is not portable for a SUM(60/period) across Postgres and SQLite). Regions is needed because a multi-region check executes once per region per period, so its per-minute cost is (60s/period) × max(1, len(Regions)). Type is needed to exclude passive types (heartbeat, email) from the demand measured against MaxChecksPerMinute: they return before the token gate and consume no execution budget (spec 2026-08-26-03).
type CheckStatus ¶
type CheckStatus int
CheckStatus represents the health status of a check.
const ( // CheckStatusCreated indicates the check was just created and hasn't been executed yet. CheckStatusCreated CheckStatus = 1 // CheckStatusUp indicates the check is healthy. CheckStatusUp CheckStatus = 3 // CheckStatusDown indicates the check is failing. CheckStatusDown CheckStatus = 4 // CheckStatusValidating is the transient state between "first failure // observed" and "incident opens" — the failure has been seen but the // configured ConfirmationPeriod hasn't elapsed yet. Display-only: // never triggers notifications, never gates the incident state machine. CheckStatusValidating CheckStatus = 5 // CheckStatusDegraded is the aggregated/summary status: a rolled-up window // contained warning(s) but no dominating failure. Not produced by the live // pipeline (which uses CheckStatusWarning); retained for rendering a // check's aggregated/summary status and as a valid ?status= filter value. CheckStatusDegraded CheckStatus = 7 // CheckStatusWarning is the live current status: the target is up but // there is something to report. Display-only like CheckStatusValidating — // never triggers notifications, never gates the incident state machine. CheckStatusWarning CheckStatus = 8 )
func RollupGroupStatus ¶
func RollupGroupStatus(counts map[CheckStatus]int) CheckStatus
RollupGroupStatus derives a single, read-time status for a check group from the per-status counts of its considered member checks (spec 2026-08-01-01). Callers must pre-filter counts to enabled, non-deleted members — this function has no opinion on which checks are "in" the group, only on how to combine their statuses.
Deliberately Better-Stack-shaped: partial failure reads as "degraded" (not a blanket "down"), and a warning-only member doesn't paint the whole group red. Rules, in priority order:
- No considered members (or the map only contains CheckStatusCreated entries) → CheckStatusCreated.
- All considered members down → CheckStatusDown.
- Some but not all down → CheckStatusDegraded.
- No down, at least one warning → CheckStatusWarning.
- No down/warning, at least one validating → CheckStatusValidating.
- Otherwise, at least one up → CheckStatusUp.
- Falls back to CheckStatusCreated (e.g. members whose status doesn't match any of the above — in practice only CheckStatusCreated, since CheckStatusDegraded is never a live per-check status).
func (CheckStatus) String ¶
func (s CheckStatus) String() string
String returns the lowercase wire name for a CheckStatus, used by the dashboard to key status colors and labels. Unknown values fall back to "unknown" so an unset DB column never blows up the UI.
type CheckStatusCount ¶
type CheckStatusCount struct {
Status CheckStatus `bun:"status"`
Enabled bool `bun:"enabled"`
Count int `bun:"count"`
}
CheckStatusCount is one row of the org-wide check aggregation (spec 2026-08-02-06): the number of checks sharing a (status, enabled) pair. Produced by db.Service.GetCheckStatusCounts on both dialects and folded into the checks stats response.
type CheckUpdate ¶
type CheckUpdate struct {
CheckGroupUID *string
Name *string
Slug *string
Description *string
Type *string
Config *JSONMap
ConfigPrivate *string
ConfigPrivateKeys *string
ClearConfigPrivate bool
ConfigSealed *string
ClearConfigSealed bool
Regions *[]string
Enabled *bool
Internal *bool
Period *timeutils.Duration
// RegionSpread sets the inter-region offset override; ClearRegionSpread
// resets it to NULL (revert to the period/region_count default).
RegionSpread *timeutils.Duration
ClearRegionSpread bool
// Incident tracking — wall-clock periods replacing the legacy count
// thresholds. EscalationThreshold stays count-based for now.
ConfirmationPeriodSeconds *int
RecoveryPeriodSeconds *int
EscalationThreshold *int
// FirstFailureAt / FirstSuccessSinceFailureAt drive the open/resolve
// clocks; ProcessCheckResult sets/clears them as the streak signal flips.
FirstFailureAt *time.Time
FirstSuccessSinceFailureAt *time.Time
ClearFirstFailureAt bool
ClearFirstSuccessSinceFailureAt bool
// Adaptive resolution settings
ReopenCooldownMultiplier *int
// Flapping (adaptive recovery) config — spec 2026-06-30-07.
FlappingWindowSeconds *int
FlapBackoffFactor *int
MaxRecoveryMultiplier *int
// Optional escalation policy override (nil = inherit from group / none)
EscalationPolicyUID *string
// TracerouteOnFailure sets the per-check path-trace override;
// ClearTracerouteOnFailure resets it to NULL (inherit the org default).
TracerouteOnFailure *bool
ClearTracerouteOnFailure bool
// Clear* fields set the corresponding column to NULL on update.
ClearEscalationPolicyUID bool
// Status tracking (internal use)
Status *CheckStatus
StatusStreak *int
StatusChangedAt *time.Time
}
CheckUpdate represents fields that can be updated.
type CompactResultsOutcome ¶
type CompactResultsOutcome struct {
// Fetched is the number of source rows read for the bucket.
Fetched int
// SourceCount is the number of measurable source rows the aggregate function
// selected for deletion (its returned sourceUIDs). Zero for a marker-only
// bucket.
SourceCount int
// Compacted is true when the rollup row was upserted and the source rows
// deleted (the whole transaction committed). False when there was nothing to
// compact (no fetched rows, or a marker-only bucket).
Compacted bool
// DeletedCount is the number of source rows actually deleted.
DeletedCount int64
}
CompactResultsOutcome reports what CompactResults did inside its transaction.
type ConnectionType ¶
type ConnectionType string
ConnectionType represents the type of integration connection.
const ( ConnectionTypeSlack ConnectionType = "slack" ConnectionTypeDiscord ConnectionType = "discord" ConnectionTypeWebhook ConnectionType = "webhook" ConnectionTypeEmail ConnectionType = "email" ConnectionTypeGoogleChat ConnectionType = "googlechat" ConnectionTypeMattermost ConnectionType = "mattermost" ConnectionTypeNtfy ConnectionType = "ntfy" // ConnectionTypeGotify is a self-hosted Gotify push server: a stateless // HTTP POST to {server_url}/message with the app token in the // X-Gotify-Key header, alongside ntfy/Matrix/Mattermost/Pushover in the // self-hosted/homelab notification lineup. ConnectionTypeGotify ConnectionType = "gotify" ConnectionTypePagerduty ConnectionType = "pagerduty" ConnectionTypePushover ConnectionType = "pushover" ConnectionTypeFreebox ConnectionType = "freebox" ConnectionTypeWebPush ConnectionType = "webpush" ConnectionTypeKubernetes ConnectionType = "kubernetes" ConnectionTypeTwilio ConnectionType = "twilio" ConnectionTypeMSTeams ConnectionType = "msteams" // ConnectionTypeMSTeamsBot is the two-way Microsoft Teams bot integration // (Azure Bot / Bot Framework). It is deliberately distinct from // ConnectionTypeMSTeams, which stays as the zero-infra, one-way Teams // Workflow webhook: the two coexist and an org may use either or both. ConnectionTypeMSTeamsBot ConnectionType = "msteams-bot" // ConnectionTypeMatrix is the org-level Matrix (matrix.org) integration: a // stateless HTTP sender to a Matrix room via the Client-Server API, // alongside Slack/Discord/ntfy — not the instance-level direct-channel // path used by Telegram. ConnectionTypeMatrix ConnectionType = "matrix" // ConnectionTypeZulip is the Zulip chat integration: a stateless HTTP POST // to {site_url}/api/v1/messages via Zulip's bot API, with every lifecycle // event of one incident landing in the same topic (see // notifications.zulipTopic) so Zulip threads the incident automatically — // the same "one thread per incident" outcome Slack works hard to emulate // with reverse threads. ConnectionTypeZulip ConnectionType = "zulip" )
Connection types.
type DeliveryDetails ¶
type DeliveryDetails struct {
// HTTPStatusCode is the HTTP response status (e.g. 200, 503). Zero/omitted
// for non-HTTP channels or when no response was received.
HTTPStatusCode int `json:"httpStatusCode,omitempty"`
// RequestURL is the target URL with the query string and any credentials
// stripped (host + path only). Never the raw URL.
RequestURL string `json:"requestUrl,omitempty"`
// RequestBody is the payload sent to the receiver, capped at
// DeliveryDetailsBodyCap bytes.
RequestBody string `json:"requestBody,omitempty"`
// ResponseBody is the receiver's response body, capped at
// DeliveryDetailsBodyCap bytes.
ResponseBody string `json:"responseBody,omitempty"`
// DurationMs is the wall-clock time of the delivery attempt in milliseconds.
DurationMs int64 `json:"durationMs,omitempty"`
// ResponseHeaders is a small allowlisted set of response headers (e.g.
// Retry-After, Content-Type). Never carries request-side secret headers.
ResponseHeaders map[string]string `json:"responseHeaders,omitempty"`
}
DeliveryDetails holds structured, per-attempt notification delivery artifacts. It is stored as JSON (Postgres jsonb / SQLite text) in incident_notifications.delivery_details. Every field is optional: a channel populates only what it can produce, and a missing field is simply omitted.
Privacy: callers MUST never place the webhook signing secret, an Authorization header, or any other secret into this struct. RequestURL must be stripped of its query string and userinfo before being set.
func (*DeliveryDetails) Scan ¶
func (d *DeliveryDetails) Scan(value any) error
Scan implements sql.Scanner for reading the JSON blob back from either engine.
func (DeliveryDetails) Value ¶
func (d DeliveryDetails) Value() (driver.Value, error)
Value implements driver.Valuer so DeliveryDetails persists as a JSON string on both Postgres (jsonb) and SQLite (text). A nil pointer is handled by the driver as NULL before this is called; on a non-nil empty value it stores "{}".
type DeviceAuthRequest ¶
type DeviceAuthRequest struct {
bun.BaseModel `bun:"table:device_auth_requests,alias:device_auth_request"`
UID string `bun:"uid,pk,type:varchar(36)"`
DeviceCode string `bun:"device_code,notnull"`
UserCode string `bun:"user_code,notnull"`
ClientName string `bun:"client_name,notnull"`
Status string `bun:"status,notnull"`
OrganizationUID *string `bun:"organization_uid"`
UserUID *string `bun:"user_uid"`
TokenUID *string `bun:"token_uid"`
TokenValue *string `bun:"token_value"`
LastPolledAt *time.Time `bun:"last_polled_at"`
ExpiresAt time.Time `bun:"expires_at,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}
DeviceAuthRequest is a pending OAuth 2.0 Device Authorization Grant request (RFC 8628) opened by a CLI and approved by a human in the dashboard.
DeviceCode is the requesting client's secret and the real capability (32 random bytes); UserCode is the short, human-typed code and therefore the brute-forceable surface — the consent lookup is rate limited on top of the global limiter. TokenValue holds the PAT minted at consent time until the client's next poll picks it up, at which point the whole row is deleted so the token can only ever be delivered once.
OrganizationUID is the org the approving user SELECTED on the consent page, not implicitly their session org: solidping is multi-tenant and the minted PAT is scoped to that choice.
func NewDeviceAuthRequest ¶
func NewDeviceAuthRequest(clientName, deviceCode, userCode string, expiresAt time.Time) *DeviceAuthRequest
NewDeviceAuthRequest builds a pending request with a generated UID. deviceCode and userCode are caller-generated; userCode must already be in canonical (uppercase, dashless) form.
type DeviceAuthResolution ¶
type DeviceAuthResolution struct {
Status string
UserUID string
OrganizationUID string
TokenUID string
TokenValue string
}
DeviceAuthResolution carries the outcome of a consent decision. Status is DeviceAuthStatusApproved or DeviceAuthStatusDenied; the token fields are set only on approval.
type DiscordSettings ¶
type DiscordSettings struct {
// WebhookURL is the legacy, one-way mode. Kept first and unchanged so a
// pre-bot row decodes exactly as it always did.
WebhookURL string `json:"webhook_url,omitempty"`
// GuildID / GuildName identify the Discord server the bot was installed
// into. GuildID is the identity the org mapping is keyed on (see
// organization_providers, ProviderTypeDiscord).
GuildID string `json:"guild_id,omitempty"`
GuildName string `json:"guild_name,omitempty"`
// BotUserID is the application's own user id inside the guild, used to
// recognize "this message is ours" on the Gateway.
BotUserID string `json:"bot_user_id,omitempty"`
// ChannelID / ChannelName are the default notification destination — the
// Discord counterpart of SlackSettings.ChannelID.
ChannelID string `json:"channel_id,omitempty"`
ChannelName string `json:"channel_name,omitempty"`
// InstalledByUserID records the Discord user who performed the install.
InstalledByUserID string `json:"installed_by_user_id,omitempty"`
// MentionOnCall makes channel alerts ping the humans the escalation policy
// would page first (`<@123>`). Zero value is deliberately false so every
// integration stored before this field existed keeps behaving exactly as
// before; the bot install flow writes `true` explicitly for new installs.
MentionOnCall bool `json:"mention_on_call,omitempty"`
// CommentIngestion selects how inbound Discord thread replies are treated,
// with the same explicit/all semantics as SlackSettings.CommentIngestion.
// Zero value ("") means explicit — the safe direction.
CommentIngestion string `json:"comment_ingestion,omitempty"`
}
DiscordSettings represents Discord-specific settings stored in the Settings JSONB. It covers BOTH Discord modes, and which one a connection is in is a function of the data, never of a separate connection type:
- Legacy webhook mode: only WebhookURL is set. This is what every Discord integration created before the bot existed looks like, and it keeps working untouched — the sender picks the webhook path whenever the bot fields are absent, with no migration and no re-install.
- Bot mode: GuildID + ChannelID are set (written by the bot install flow). Threads, message edits, the Acknowledge button, mentions and inbound comments are only available here.
No credential lives in this blob: the bot token is instance-level system config (`auth.discord.bot_token`), exactly like the Teams bot's Entra app secret, so a stolen settings blob grants nothing.
func DiscordSettingsFromJSONMap ¶
func DiscordSettingsFromJSONMap(m JSONMap) (*DiscordSettings, error)
DiscordSettingsFromJSONMap parses DiscordSettings from a JSONMap.
func (*DiscordSettings) IngestsAllThreadReplies ¶
func (ds *DiscordSettings) IngestsAllThreadReplies() bool
IngestsAllThreadReplies reports whether this Discord integration captures every human thread reply as an incident comment. Anything other than an explicit "all" — including an absent value on a pre-existing row — means no.
func (*DiscordSettings) ToJSONMap ¶
func (ds *DiscordSettings) ToJSONMap() (JSONMap, error)
ToJSONMap converts DiscordSettings to JSONMap for storage.
func (*DiscordSettings) UsesBot ¶
func (ds *DiscordSettings) UsesBot() bool
UsesBot reports whether this connection is in bot mode. A guild and a target channel are both required: a guild with no channel has nowhere to post, and the sender must fall back to the webhook rather than silently doing nothing.
type DiscoveredCheck ¶
type DiscoveredCheck struct {
UID string `bun:"uid,pk,type:varchar(36)" json:"uid"`
OrganizationUID string `bun:"organization_uid,notnull" json:"organizationUid"`
JobUID string `bun:"job_uid,notnull" json:"jobUid"`
Source DiscoverySource `bun:"source,notnull" json:"source"`
GroupKey string `bun:"group_key,notnull" json:"groupKey"`
GroupLabel string `bun:"group_label,notnull" json:"groupLabel"`
Name string `bun:"name,notnull" json:"name"`
Slug string `bun:"slug,notnull" json:"slug"`
Type string `bun:"type,notnull" json:"type"`
Config json.RawMessage `bun:"config,type:jsonb" json:"config"`
Metadata json.RawMessage `bun:"metadata,type:jsonb" json:"metadata,omitempty"`
PromotedToCheckUID *string `bun:"promoted_to_check_uid" json:"promotedToCheckUid,omitempty"` //nolint:lll // aligned tag width
DiscoveredAt time.Time `bun:"discovered_at,notnull,default:current_timestamp" json:"discoveredAt"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"createdAt"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp" json:"updatedAt"`
DeletedAt *time.Time `bun:"deleted_at,soft_delete" json:"deletedAt,omitempty"`
}
DiscoveredCheck is one suggested check produced by a discovery scan. Rows are grouped for display by GroupKey (an IP, container ID, workload UID…) — the group is purely a rendering concern; the stored unit is the check itself.
func NewDiscoveredCheck ¶
func NewDiscoveredCheck( orgUID, jobUID string, source DiscoverySource, groupKey, groupLabel, name, slug, checkType string, config, meta json.RawMessage, ) *DiscoveredCheck
NewDiscoveredCheck builds a DiscoveredCheck row from the fully-formed pieces a suggester emits within a group. config and meta may be nil; config defaults to "{}" so the NOT NULL column is satisfied.
type DiscoverySource ¶
type DiscoverySource string
DiscoverySource identifies which discovery mechanism produced a suggested check. It is a named string (matching the ProviderType / JobType convention) so the set of sources is closed in code while staying a plain TEXT column.
const ( // DiscoverySourceLAN marks a check suggested by the CIDR network scanner. DiscoverySourceLAN DiscoverySource = "lan" // DiscoverySourceFreebox marks a check suggested via the Freebox LAN browser. DiscoverySourceFreebox DiscoverySource = "freebox" // DiscoverySourceContainer marks a container found on a configured // Docker-compatible host (Docker or Podman). DiscoverySourceContainer DiscoverySource = "container" // DiscoverySourceKubernetes marks a Deployment/ReplicaSet found on a // configured Kubernetes cluster connection. DiscoverySourceKubernetes DiscoverySource = "kubernetes" )
Supported discovery sources. Additional sources (container, kubernetes) are registered by their own specs; the column has no DB CHECK constraint so new values are Go-level only.
type EmailSuppression ¶
type EmailSuppression struct {
bun.BaseModel `bun:"table:email_suppressions,alias:email_suppression"`
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
Email string `bun:"email,notnull"`
CheckUID *string `bun:"check_uid"` // NULL = suppresses all checks in the org
Source string `bun:"source,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
}
EmailSuppression records that a recipient address has opted out of incident/alert emails, either for one specific check (CheckUID set) or for every check in the org (CheckUID nil). Backed by the `email_suppressions` table. Transactional emails (registration, password reset, invitation, password-changed) never consult this table — suppression only applies to incident/alert emails (spec acceptance criterion 6).
func NewEmailSuppression ¶
func NewEmailSuppression(orgUID, email string, checkUID *string, source string) *EmailSuppression
NewEmailSuppression creates a new suppression row with a generated UID. checkUID is nil for an org-wide suppression.
type EntitlementLimits ¶
type EntitlementLimits struct {
MaxChecks *int `json:"maxChecks,omitempty"`
MaxUsers *int `json:"maxUsers,omitempty"`
MaxChecksPerMinute *int `json:"maxChecksPerMinute,omitempty"`
// MaxDeportedAgents caps the org's active deported (private-location)
// agents across all private regions. nil = unlimited.
MaxDeportedAgents *int `json:"maxDeportedAgents,omitempty"`
// MaxCustomDomains caps the org's status pages served on a customer-owned
// domain. nil = unlimited (self-hosted default); SaaS defaults to 0 and
// billing raises it per plan.
MaxCustomDomains *int `json:"maxCustomDomains,omitempty"`
// MaxSmsPerMonth / MaxCallsPerMonth cap the org's outbound SMS and voice
// calls per UTC calendar month. nil = unlimited (self-hosted default,
// bring-your-own Twilio); SaaS defaults to 0 and billing raises it per plan.
MaxSmsPerMonth *int `json:"maxSmsPerMonth,omitempty"`
MaxCallsPerMonth *int `json:"maxCallsPerMonth,omitempty"`
// MaxWhatsappPerMonth caps the org's outbound WhatsApp template messages
// per UTC calendar month. nil = unlimited (self-hosted default, the
// operator brings their own WABA); SaaS defaults to 0 and billing raises
// it per plan.
MaxWhatsappPerMonth *int `json:"maxWhatsappPerMonth,omitempty"`
// MaxSlos caps the org's service-level objectives (spec 2026-08-20-01).
// nil = unlimited (self-hosted default); SaaS defaults to 2 and billing
// raises it per plan.
MaxSlos *int `json:"maxSlos,omitempty"`
// WhiteLabel is the one non-numeric entitlement: whether the org may drop
// the "powered by SolidPing" badge from its status pages (spec
// 2026-08-21-07). It lives here rather than in a sibling struct because
// this IS the entitlement payload billing writes, and splitting the shape
// in two would mean a second wire contract to rotate.
//
// nil means "use the deployment default" — true self-hosted (nobody should
// have to pay to unbrand their own instance), false on the SaaS, where
// paying is what unlocks it. Note the asymmetry with the *int fields: nil
// there means UNLIMITED, here it means DEFAULT, because a boolean has no
// "unbounded" reading.
WhiteLabel *bool `json:"whiteLabel,omitempty"`
}
EntitlementLimits is the quantitative half of an entitlement set. nil = unlimited. JSON tags are the wire format consumed by the API.
Four limits are modeled: MaxChecks (total non-internal checks an org may own, enforced at check creation), MaxUsers (total org members — capped on self-hosted by default), MaxChecksPerMinute (aggregate dispatch rate, capped on SaaS), and MaxDeportedAgents (active deported / private-location agents across all private regions, enforced at enrollment — see AgentCreateAllowed). Adding an optional field to the v1 JSONB payload is backward-compatible — absent keys unmarshal to nil (= unlimited), so no version bump is needed.
MaxUsers is decoded from either the canonical `maxUsers` key or the deprecated `maxSsoUsers` alias (see UnmarshalJSON) so already-deployed billing services and stored v1 rows that still use the old key keep working forever. It is always marshaled back as `maxUsers`.
func (*EntitlementLimits) UnmarshalJSON ¶
func (l *EntitlementLimits) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes the limits object, accepting `maxSsoUsers` as a deprecated decode-only alias for `maxUsers`. Sending both is an error. Unknown limit keys are still rejected so typos surface loudly, matching the DisallowUnknownFields contract the PUT decoder used before this type grew a custom unmarshaler.
type EntitlementSource ¶
type EntitlementSource string
EntitlementSource identifies who wrote the row.
const ( EntitlementSourceDefault EntitlementSource = "default" EntitlementSourceSelfHosted EntitlementSource = "self-hosted" EntitlementSourceAdmin EntitlementSource = "admin" EntitlementSourceOrgAdmin EntitlementSource = "org-admin" EntitlementSourceBilling EntitlementSource = "billing-service" )
Entitlement source labels.
EntitlementSourceAdmin and EntitlementSourceOrgAdmin look alike and are NOT interchangeable — the difference is which door the write came through, and that decides whether it outranks billing:
- `admin` is minted ONLY by the instance-level superadmin editor (`PUT /api/v1/system/entitlements/:org`). It is an override: it resolves whole-row (nil = unlimited) and suppresses billing pushes until it is explicitly released.
- `org-admin` is minted by the org-scoped `PUT /api/v1/orgs/:org/entitlements` when an ORG admin (not a superadmin) writes it — the self-hosted operator's door, gated by `entitlements.admin_writes_enabled`. It behaves exactly as `admin` did before spec 2026-08-26-06: same paid plan weight, ordinary null-fill resolution, and billing's next reconcile overwrites it.
Collapsing the two would let any org admin on a SaaS install that never set SP_ENTITLEMENTS_ADMIN_WRITES grant themselves limits AND lock the billing service out of correcting them, which no superadmin ever authorized.
Migration note: rows already stored as `admin` were written through the org-scoped door and will read as superadmin overrides from now on. There are only a handful, they are visible in the superadmin editor, and releasing one is a single click — so they are left alone rather than rewritten blind.
type EntitlementsPayload ¶
type EntitlementsPayload struct {
Version int `json:"version"`
Source EntitlementSource `json:"source,omitempty"`
Limits EntitlementLimits `json:"limits"`
// DisplayName / DisplayEmoji are billing-supplied plan identity, shown
// in the dashboard (e.g. "🚀 Team"). Display-only — never enforced.
DisplayName *string `json:"displayName,omitempty"`
DisplayEmoji *string `json:"displayEmoji,omitempty"`
}
EntitlementsPayload is the structured-by-OSS portion of an org_entitlements row, stored as JSON in the `payload` column. The struct itself is the schema; absent keys mean "use default" and extra keys are silently ignored for forward-compat. The Version field gates shape-migrations at unmarshal time.
func (*EntitlementsPayload) Scan ¶
func (p *EntitlementsPayload) Scan(value any) error
Scan implements sql.Scanner. Empty / NULL values yield a zero-valued payload with the current version stamped in — the resolver will fall back to defaults for absent fields.
func (*EntitlementsPayload) UnmarshalJSON ¶
func (p *EntitlementsPayload) UnmarshalJSON(data []byte) error
UnmarshalJSON probes the version discriminator first and dispatches to the matching shape-migration. v0 (rows written before the version field landed) is treated as v1.
type EscalationPolicy ¶
type EscalationPolicy struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
Name string `bun:"name,notnull"`
Description *string `bun:"description"`
RepeatMax int `bun:"repeat_max,notnull"`
RepeatAfterSeconds *int `bun:"repeat_after_seconds"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
EscalationPolicy is a reusable orchestration of paging steps. Distinct from check_connections (per-check broadcast). The check or its group references one policy via escalation_policy_uid.
func NewEscalationPolicy ¶
func NewEscalationPolicy(orgUID, name string) *EscalationPolicy
NewEscalationPolicy builds a policy with a fresh UID.
type EscalationPolicyStep ¶
type EscalationPolicyStep struct {
UID string `bun:"uid,pk,type:varchar(36)"`
PolicyUID string `bun:"policy_uid,notnull"`
Position int `bun:"position,notnull"`
DelaySeconds int `bun:"delay_seconds,notnull"`
// SeverityUID points to the severity that decides the channel-set
// for this step. NULL = "fall back to the org default severity for
// user/all_admins targets, or the connection's own channel for
// connection targets". Spec 2026-05-08-03.
SeverityUID *string `bun:"severity_uid"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}
EscalationPolicyStep is one rung of a policy. Delays are between adjacent steps (see spec): inserting a step in the middle does not require recomputing downstream delays.
func NewEscalationPolicyStep ¶
func NewEscalationPolicyStep(policyUID string, position, delaySeconds int) *EscalationPolicyStep
NewEscalationPolicyStep builds a step row with a fresh UID.
type EscalationPolicyTarget ¶
type EscalationPolicyTarget struct {
UID string `bun:"uid,pk,type:varchar(36)"`
StepUID string `bun:"step_uid,notnull"`
TargetType EscalationTargetType `bun:"target_type,notnull"`
TargetUID *string `bun:"target_uid"`
Position int `bun:"position,notnull"`
}
EscalationPolicyTarget is one recipient inside a step. Multiple targets per step fire in parallel.
func NewEscalationPolicyTarget ¶
func NewEscalationPolicyTarget( stepUID string, targetType EscalationTargetType, targetUID *string, position int, ) *EscalationPolicyTarget
NewEscalationPolicyTarget builds a target row with a fresh UID.
type EscalationPolicyUpdate ¶
type EscalationPolicyUpdate struct {
Name *string
Description *string
RepeatMax *int
RepeatAfterSeconds *int
ClearDescription bool
ClearRepeatAfterSeconds bool
}
EscalationPolicyUpdate captures the writable fields. Pointer = optional.
type EscalationTargetType ¶
type EscalationTargetType string
EscalationTargetType is the kind of recipient a policy step pages.
const ( // EscalationTargetUser pages a specific user via their preferred channels. EscalationTargetUser EscalationTargetType = "user" // EscalationTargetSchedule pages whoever the on-call resolver returns at fire time. EscalationTargetSchedule EscalationTargetType = "schedule" // EscalationTargetConnection fires a specific notification connection. EscalationTargetConnection EscalationTargetType = "connection" // EscalationTargetAllAdmins pages every admin member of the organization. EscalationTargetAllAdmins EscalationTargetType = "all_admins" )
type Event ¶
type Event struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
IncidentUID *string `bun:"incident_uid"`
CheckUID *string `bun:"check_uid"`
JobUID *string `bun:"job_uid"`
EventType EventType `bun:"event_type,notnull"`
ActorType ActorType `bun:"actor_type,notnull"`
// ActorUID is the acting user's UID — this column IS the spec's
// `actor_user_uid` (2026-08-21-09). It predates that spec as an FK to
// users(uid); a second column of the same meaning would only create a
// split brain, so the API exposes it under the `actorUserUid` parameter
// name while the column keeps its original name.
ActorUID *string `bun:"actor_uid"`
Payload JSONMap `bun:"payload,type:jsonb,nullzero"`
// SourceIP is the client address the action came from, when the request
// had one and audit.capture_ip is on. Nil for system-originated events and
// for deployments that turned IP capture off for GDPR reasons.
SourceIP *string `bun:"source_ip"`
// UserAgent is the raw User-Agent header, truncated. Nil when absent.
UserAgent *string `bun:"user_agent"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
}
Event represents an audit log entry.
type EventType ¶
type EventType string
EventType represents the type of an audit event.
const ( // EventTypeCheckCreated indicates a check was created. EventTypeCheckCreated EventType = "check.created" // EventTypeCheckUpdated indicates a check was updated. EventTypeCheckUpdated EventType = "check.updated" // EventTypeCheckDeleted indicates a check was deleted. EventTypeCheckDeleted EventType = "check.deleted" // EventTypeIncidentCreated indicates an incident was created. EventTypeIncidentCreated EventType = "incident.created" // EventTypeIncidentEscalated indicates an incident was escalated. EventTypeIncidentEscalated EventType = "incident.escalated" // EventTypeIncidentEscalationFailed indicates an escalation step // could not be delivered (empty schedule, missing user, etc.). Soft // failure — subsequent steps still fire. EventTypeIncidentEscalationFailed EventType = "incident.escalation_failed" // EventTypeIncidentResolved indicates an incident was resolved. EventTypeIncidentResolved EventType = "incident.resolved" // EventTypeIncidentReopened indicates an incident was reopened after a relapse. EventTypeIncidentReopened EventType = "incident.reopened" // EventTypeIncidentRolledUp indicates an already-open incident was // retroactively attached to a hard parent's incident and its paging // suppressed, because the parent confirmed AFTER it did (spec // 2026-08-24-15). Distinct from the silent attachment that happens at // incident-open time: this one changes a live incident's paging behavior // mid-flight, so the timeline has to say so. Never pages. EventTypeIncidentRolledUp EventType = "incident.rolled_up" // EventTypeIncidentRollupDetached indicates a suppressed child was // un-attached from its rollup parent because the parent resolved AND the // child's own check had already recovered (spec // 2026-08-31-07-rollup-detach-erases-attribution). paging_suppressed // flips false, but caused_by_incident_uid is deliberately kept — it is // the post-mortem record of which cascade this incident belonged to. // Never pages: the child never paged on the way in, so it does not page // on the way out either. EventTypeIncidentRollupDetached EventType = "incident.rollup_detached" // EventTypeIncidentAcknowledged indicates an incident was acknowledged. EventTypeIncidentAcknowledged EventType = "incident.acknowledged" // EventTypeIncidentUnacknowledged indicates an acknowledgment was cleared. EventTypeIncidentUnacknowledged EventType = "incident.unacknowledged" // EventTypeIncidentSnoozed indicates an incident was snoozed until a future time. EventTypeIncidentSnoozed EventType = "incident.snoozed" // EventTypeIncidentUnsnoozed indicates an incident's snooze was cleared. EventTypeIncidentUnsnoozed EventType = "incident.unsnoozed" // EventTypeIncidentComment is a free-text, user-authored comment on an // incident, ingested from the dashboard or a Slack thread reply. The // payload carries `text`, `source` (web|slack) and, for Slack-authored // comments, the Slack author attribution (slackUserId, slackUserName, // slackTeamId, slackTs). Append-only, like every other event row. EventTypeIncidentComment EventType = "incident.comment" // EventTypeStatusPageIncidentPublished indicates an incident became visible // on a status page (spec 2026-08-19-08). It is deliberately DISTINCT from // EventTypeIncidentCreated: an operational incident opening and a // customer-visible incident being published are different facts, they // happen at different times (the auto-publish debounce sits between them), // and a great many incidents never produce the second one at all. A // webhook consumer must be able to subscribe to one without the other. EventTypeStatusPageIncidentPublished EventType = "statuspage.incident.published" // EventTypeStatusPageIncidentUpdated indicates a publication's public // title, severity, state or narrative changed. EventTypeStatusPageIncidentUpdated EventType = "statuspage.incident.updated" // EventTypeStatusPageIncidentResolved indicates a publication was closed // (or unpublished). The internal incident.resolved event is unchanged and // still fires on its own schedule. EventTypeStatusPageIncidentResolved EventType = "statuspage.incident.resolved" // EventTypeStatusSubscriberDisabled indicates a webhook/Slack status-page // subscription was disabled after repeated delivery failures (spec // 2026-08-21-07). Without it the only symptom of a broken webhook is // "we stopped getting notifications" — which nobody notices until an // incident. EventTypeStatusSubscriberDisabled EventType = "statuspage.subscriber.disabled" // EventTypeStatusPageCustomDomainDemoted indicates a status page's custom // domain stayed unreachable well past its grace window and stopped being // served (spec 2026-08-23-03). Until this existed the only symptom was a // customer's status page going dark, discovered — during an outage — by // the people the page exists to inform. Entering `grace` does NOT emit // this: the page is still serving there and paging for it would teach // operators to ignore the one that matters. EventTypeStatusPageCustomDomainDemoted EventType = "statuspage.custom_domain.demoted" // EventTypeStatusUpdateCreated indicates a status update was created. EventTypeStatusUpdateCreated EventType = "status_update.created" // EventTypeStatusUpdateUpdated indicates a status update was modified. EventTypeStatusUpdateUpdated EventType = "status_update.updated" // EventTypeStatusUpdateDeleted indicates a status update was soft-deleted. EventTypeStatusUpdateDeleted EventType = "status_update.deleted" // EventTypeOrgActivationSignupCompleted fires once per org when its // initial member is created (the user who provisioned the org). EventTypeOrgActivationSignupCompleted EventType = "org.activation.signup_completed" // EventTypeOrgActivationFirstCheckCreated fires once per org the first // time a check is created for it. EventTypeOrgActivationFirstCheckCreated EventType = "org.activation.first_check_created" // EventTypeOrgActivationFirstResultReceived fires once per org the first // time a check result is recorded for it. EventTypeOrgActivationFirstResultReceived EventType = "org.activation.first_result_received" // EventTypeOrgActivationFirstNotificationConfigured fires once per org // the first time an integration connection is created for it. EventTypeOrgActivationFirstNotificationConfigured EventType = "org.activation.first_notification_configured" // EventTypeOrgActivationFirstIncidentPaged fires once per org the first // time an incident notification is dispatched for it. EventTypeOrgActivationFirstIncidentPaged EventType = "org.activation.first_incident_paged" // EventTypeAuthLoginSucceeded records a successful authentication. The // payload carries `auth_method` — a local first factor (password / ldap / // passkey), a named federated connector (oidc / saml / github / …), a // composite 2FA form ("password+totp"), or one of the local // session-minting paths (invitation / registration / switch_org / // org_session). Never any credential. // // Emitted from auth.Service.startSession, the single point at which a // session row is created, so no login path can skip it. EventTypeAuthLoginSucceeded EventType = "auth.login_succeeded" // EventTypeAuthLoginFailed records a rejected authentication attempt. It is // a brute-force amplification vector, so it is the one event type that is // NOT written one-row-per-occurrence: repeats of the same (org, email, IP) // inside a short window fold into a single row with a `count`, and a // per-org hourly ceiling caps how many rows can be created at all. See // internal/audit/loginfailed.go. EventTypeAuthLoginFailed EventType = "auth.login_failed" // EventTypeAuthLogout records a session being ended deliberately. EventTypeAuthLogout EventType = "auth.logout" // EventTypeAuthTokenCreated records an API token or agent enrollment key // being minted. The payload carries the token's NAME and PREFIX only — // never the value, which the server itself only ever sees hashed. EventTypeAuthTokenCreated EventType = "auth.token_created" // EventTypeAuthTokenRevoked records an API token or agent key being revoked. EventTypeAuthTokenRevoked EventType = "auth.token_revoked" // EventTypeAuthTokenMisuse records a credential being presented by a party // it was not issued to — today, an OAuth client asking to revoke a grant // that belongs to a different client. // // Deliberately NOT a variant of auth.token_revoked with a `result` field: // auth.token_revoked must mean "a grant was revoked", full stop, or every // reader of the trail has to check a discriminator before believing it. // This is a different fact — an attempt, not an outcome — and an org // should be able to alert on it on its own. EventTypeAuthTokenMisuse EventType = "auth.token_misuse" // EventTypeMemberInvited records an invitation being sent. EventTypeMemberInvited EventType = "member.invited" // EventTypeMemberJoined records a membership row being created — whether by // an accepted invitation or by an admin adding someone directly. EventTypeMemberJoined EventType = "member.joined" // EventTypeMemberRemoved records a membership being revoked. EventTypeMemberRemoved EventType = "member.removed" // EventTypeMemberRoleChanged records a role moving. Emitted only when the // role actually changed, so a no-op PATCH does not manufacture an event. EventTypeMemberRoleChanged EventType = "member.role_changed" // EventTypeIntegrationCreated records a notification integration being added. EventTypeIntegrationCreated EventType = "integration.created" // EventTypeIntegrationUpdated records an integration being edited. The // payload lists changed field names; credential-bearing fields are named // but never valued. EventTypeIntegrationUpdated EventType = "integration.updated" // EventTypeIntegrationDeleted records an integration being removed. EventTypeIntegrationDeleted EventType = "integration.deleted" // EventTypeEscalationPolicyCreated records an escalation policy being added. EventTypeEscalationPolicyCreated EventType = "escalation_policy.created" // EventTypeEscalationPolicyUpdated records an escalation policy being edited. EventTypeEscalationPolicyUpdated EventType = "escalation_policy.updated" // EventTypeEscalationPolicyDeleted records an escalation policy being removed. EventTypeEscalationPolicyDeleted EventType = "escalation_policy.deleted" // EventTypeOnCallScheduleCreated records an on-call schedule being added. EventTypeOnCallScheduleCreated EventType = "oncall_schedule.created" // EventTypeOnCallScheduleUpdated records an on-call schedule being edited. EventTypeOnCallScheduleUpdated EventType = "oncall_schedule.updated" // EventTypeOnCallScheduleDeleted records an on-call schedule being removed. EventTypeOnCallScheduleDeleted EventType = "oncall_schedule.deleted" // EventTypeStatusPageCreated records a status page being added. Distinct // from the statuspage.incident.* family, which is about what a page // PUBLISHES rather than about the page's own configuration. EventTypeStatusPageCreated EventType = "status_page.created" // EventTypeStatusPageUpdated records a status page's configuration changing. EventTypeStatusPageUpdated EventType = "status_page.updated" // EventTypeStatusPageDeleted records a status page being removed. EventTypeStatusPageDeleted EventType = "status_page.deleted" // EventTypeMaintenanceWindowCreated records a maintenance window being added. EventTypeMaintenanceWindowCreated EventType = "maintenance_window.created" // EventTypeMaintenanceWindowUpdated records a maintenance window being edited. EventTypeMaintenanceWindowUpdated EventType = "maintenance_window.updated" // EventTypeMaintenanceWindowDeleted records a maintenance window being removed. EventTypeMaintenanceWindowDeleted EventType = "maintenance_window.deleted" // EventTypeConfigApplied records a config-as-code apply. The payload holds // the SUMMARY COUNTS (created / updated / deleted / unmanaged) and the // manifest name — deliberately never the manifest body, which routinely // carries secret references. EventTypeConfigApplied EventType = "config.applied" // EventTypeOrgSettingsUpdated records an organization-level setting change, // as a list of changed field names plus safe scalar values. EventTypeOrgSettingsUpdated EventType = "org.settings_updated" )
type File ¶
type File struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
Name string `bun:"name,notnull"`
MimeType string `bun:"mime_type,notnull"`
Size int64 `bun:"size,notnull"`
FileURI string `bun:"file_uri,notnull"`
SHA256 *string `bun:"sha256"`
CreatedBy *string `bun:"created_by"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
// Topic is the ATTACHMENT KEY (spec 2026-08-21-01): a path-like
// `<entity>/<uid>/<kind>`, e.g.
// `incidents/9a1eb273-0a95-4d6b-b967-9af076c1f8e8/screenshot`.
//
// NIL IS THE NORM. A file that is not an attachment — an org logo, a
// feedback screenshot — carries no topic and is invisible to every
// attachment query. The path shape is what makes both accesses cheap on
// one index: an exact match lists one entity's attachments of one kind, a
// prefix match reaps everything hanging off an entity when it is deleted.
Topic *string `bun:"topic"`
// Details is a free metadata bag for the attachment kind — for a
// screenshot: capturedAt, region, checkUid, trigger. Unconstrained on
// purpose so the next attachment kind needs no migration.
//
// SECURITY: operator-facing evidence, exactly like incidents.details. It
// must never be serialized onto a public surface.
Details JSONMap `bun:"details,type:jsonb,nullzero"`
}
File represents a stored file blob and its metadata. The actual bytes live behind the storage backend identified by FileURI's scheme (file://, s3://).
type FreeboxPrivateSettings ¶
type FreeboxPrivateSettings struct {
AppToken string `json:"appToken"`
}
FreeboxPrivateSettings carries the encrypted secret half of a Freebox connection. The app_token is permanent across Freebox reboots; we only ever store it once, on a successful pairing grant.
type FreeboxSettings ¶
type FreeboxSettings struct {
BaseURL string `json:"baseUrl"` // e.g. "http://mafreebox.freebox.fr"
AppID string `json:"appId"` // "io.solidping"
DeviceName string `json:"deviceName,omitempty"` // user-visible label on the Freebox admin
TrackID int `json:"trackId,omitempty"` // only set while pairing; cleared on grant
Status string `json:"status,omitempty"` // pairing | granted | denied | timeout
}
FreeboxSettings represents the public (queryable) side of a Freebox integration connection's Settings JSONB. The matching secret — the permanent app_token granted by the Freebox after LCD approval — lives encrypted in SettingsPrivate under the "appToken" key.
func FreeboxSettingsFromJSONMap ¶
func FreeboxSettingsFromJSONMap(m JSONMap) (*FreeboxSettings, error)
FreeboxSettingsFromJSONMap parses FreeboxSettings from a JSONMap.
func (*FreeboxSettings) ToJSONMap ¶
func (fs *FreeboxSettings) ToJSONMap() (JSONMap, error)
ToJSONMap converts FreeboxSettings to JSONMap for storage.
type Incident ¶
type Incident struct {
UID string `bun:"uid,pk,type:varchar(36)"`
// Number is the short, per-org, monotonically increasing reference rendered
// as `#42` in the dashboard, Slack and Telegram. Nobody types a 36-char UUID
// into a chat on a phone, so every human-facing surface addresses an
// incident by this instead. Assigned once at creation and never reused —
// soft-deleted incidents keep theirs.
Number int64 `bun:"number,notnull"`
OrganizationUID string `bun:"organization_uid,notnull"`
CheckUID string `bun:"check_uid,notnull"`
Region *string `bun:"region"`
State IncidentState `bun:"state,notnull"`
StartedAt time.Time `bun:"started_at,notnull"`
ResolvedAt *time.Time `bun:"resolved_at"`
ResolvedBy *string `bun:"resolved_by"`
ResolutionType *string `bun:"resolution_type"`
EscalatedAt *time.Time `bun:"escalated_at"`
AcknowledgedAt *time.Time `bun:"acknowledged_at"`
AcknowledgedBy *string `bun:"acknowledged_by"`
SnoozedUntil *time.Time `bun:"snoozed_until"`
SnoozedBy *string `bun:"snoozed_by"`
SnoozeReason *string `bun:"snooze_reason"`
FailureCount int `bun:"failure_count,notnull"`
RelapseCount int `bun:"relapse_count,notnull"`
// FlapLevel is the check's FlapCount at the moment this incident opened
// or last reopened (spec 2026-08-24-05) — the "at what flap level did
// this page fire" record. 0 = not flapping (first outage in the rolling
// window). Unlike checks.flap_count this is a point-in-time snapshot, not
// a live value, so it never needs lazy-reset handling.
FlapLevel int `bun:"flap_level,notnull"`
LastReopenedAt *time.Time `bun:"last_reopened_at"`
Title *string `bun:"title"`
Description *string `bun:"description"`
Details JSONMap `bun:"details,type:jsonb,nullzero"`
// CheckGroupUID is set on group incidents — NULL keeps the existing per-check semantics.
CheckGroupUID *string `bun:"check_group_uid"`
// CausedByIncidentUID points to the root-cause incident this one was rolled up under.
CausedByIncidentUID *string `bun:"caused_by_incident_uid"`
// PagingSuppressed gates notifications and escalation: TRUE = skip.
PagingSuppressed bool `bun:"paging_suppressed,notnull"`
// Kind is IncidentKindCheck or IncidentKindSLOBurn.
Kind string `bun:"kind,notnull"`
// SLOUID / SLOAlertPolicyUID bind a burn incident to what produced it.
// Both are NULL on a check incident. Both are `on delete set null`: losing
// the SLO must not erase the record of the pages it sent.
SLOUID *string `bun:"slo_uid"`
SLOAlertPolicyUID *string `bun:"slo_alert_policy_uid"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
Incident represents a period when a check was down.
type IncidentClockUpdate ¶
type IncidentClockUpdate struct {
FirstFailureAt *time.Time
ClearFirstFailureAt bool
FirstSuccessSinceFailureAt *time.Time
ClearFirstSuccessSinceFailureAt bool
}
IncidentClockUpdate captures the FirstFailureAt / FirstSuccessSinceFailureAt transition for a single check result. Each field is tri-state: a non-nil pointer sets the column, a true Clear* flag sets it to NULL, and the zero value (nil pointer + false flag) leaves the column untouched.
It is consumed by UpdateCheckStatusAndClocks, which writes the check's status, streak and both incident clocks in one atomic UPDATE.
type IncidentMemberCheck ¶
type IncidentMemberCheck struct {
IncidentUID string `bun:"incident_uid,pk"`
CheckUID string `bun:"check_uid,pk"`
JoinedAt time.Time `bun:"joined_at,notnull,default:current_timestamp"`
FirstFailureAt time.Time `bun:"first_failure_at,notnull"`
LastFailureAt time.Time `bun:"last_failure_at,notnull"`
LastRecoveryAt *time.Time `bun:"last_recovery_at"`
FailureCount int `bun:"failure_count,notnull"`
// No `default:true` on the tag even though the column has one — see the
// StatusPage.AutoPublishDelaySeconds note. A member row that is inserted
// already recovered is legal, and the tag silently rewrote it to failing.
CurrentlyFailing bool `bun:"currently_failing,notnull"`
}
IncidentMemberCheck tracks a single check's state inside a group incident.
type IncidentMemberUpdate ¶
type IncidentMemberUpdate struct {
LastFailureAt *time.Time
LastRecoveryAt *time.Time
FailureCount *int
CurrentlyFailing *bool
}
IncidentMemberUpdate represents fields that can be updated on a member row.
type IncidentNotification ¶
type IncidentNotification struct {
bun.BaseModel `bun:"table:incident_notifications"`
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull,type:varchar(36)"`
IncidentUID string `bun:"incident_uid,notnull,type:varchar(36)"`
EventType string `bun:"event_type,notnull"`
StepUID *string `bun:"step_uid,type:varchar(36)"`
RepeatIndex *int `bun:"repeat_index"`
Source string `bun:"source,notnull"`
UserUID *string `bun:"user_uid,type:varchar(36)"`
ConnectionUID *string `bun:"connection_uid,type:varchar(36)"`
ChannelType string `bun:"channel_type,notnull"`
Status string `bun:"status,notnull"`
SkipReason *string `bun:"skip_reason"`
Error *string `bun:"error"`
JobUID *string `bun:"job_uid,type:varchar(36)"`
MessageID *string `bun:"message_id"`
// DeliveryDetails holds structured per-attempt artifacts (HTTP status,
// stripped URL, capped request/response bodies, duration). NULL for rows
// from before the feature and for channels that produce no artifacts.
DeliveryDetails *DeliveryDetails `bun:"delivery_details,type:jsonb,nullzero"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
SentAt *time.Time `bun:"sent_at"`
CanceledAt *time.Time `bun:"cancelled_at"` //nolint:misspell // DB column uses British English
FailedAt *time.Time `bun:"failed_at"`
}
IncidentNotification records one dispatch target per event, with full lifecycle tracking (pending → sent | failed | canceled | skipped).
func NewIncidentNotificationForJob ¶
func NewIncidentNotificationForJob( orgUID, incidentUID, eventType, source, connectionUID, jobUID, channelType string, stepUID *string, repeatIndex *int, ) *IncidentNotification
NewIncidentNotificationForJob builds a pending audit row for a channel-based notification (check_connection or escalation_connection).
func NewIncidentNotificationForUser ¶
func NewIncidentNotificationForUser( orgUID, incidentUID, eventType, source, userUID, channelType string, stepUID *string, repeatIndex *int, ) *IncidentNotification
NewIncidentNotificationForUser builds a pending audit row for a direct-email escalation target (user, schedule, all_admins paths).
func NewSkippedIncidentNotification ¶
func NewSkippedIncidentNotification( orgUID, incidentUID, eventType, source, skipReason string, stepUID *string, repeatIndex *int, ) *IncidentNotification
NewSkippedIncidentNotification builds a skipped audit row for paths where no notification is sent (empty schedule, no admins, etc.).
type IncidentNotificationRow ¶
type IncidentNotificationRow struct {
IncidentNotification
// Joined from users (non-nil when user_uid IS NOT NULL)
UserName *string
// Joined from integration_connections (non-nil when connection_uid IS NOT NULL)
ConnectionName *string
ConnectionType *string
// Joined from incidents for user-scoped queries
IncidentTitle *string
IncidentState *int
IncidentStartedAt *time.Time
// Joined from checks for user-scoped queries
CheckName *string
}
IncidentNotificationRow is the read-side DTO returned by ListIncidentNotifications. It embeds the base notification row plus joined fields from users and integration_connections that are populated when the respective FK is non-NULL.
type IncidentPublication ¶
type IncidentPublication struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
// IncidentUID links the publication to the operational incident. NULL
// means the publication was authored by hand and tracks nothing.
IncidentUID *string `bun:"incident_uid"`
StatusPageUID string `bun:"status_page_uid,notnull"`
// PublicTitle is templated at creation from the resource's PUBLIC display
// name and is freely editable afterwards. It is never derived from the
// incident title, which is internal.
PublicTitle string `bun:"public_title,notnull"`
PublicState PublicationState `bun:"public_state,notnull"`
// Severity is the display-only public badge. nil = no badge.
Severity *string `bun:"severity"`
// AutoCreated marks a publication minted by the auto-publish pipeline.
// It gates group CONSOLIDATION only: the automation never appends "also
// affecting X" notes to a narrative a person wrote. Auto-RESOLVE and
// relapse REOPEN are keyed on IncidentUID instead — a publication linked to
// an incident is in scope of the page's autoResolve policy whether a
// machine or a person created it, and the two directions must agree or a
// hand-published entry gets closed by a recovery and never reopened by the
// relapse (spec 2026-09-02-05).
AutoCreated bool `bun:"auto_created,notnull"`
// HumanTouchedAt is stamped the first time a person edits the publication
// or posts an update on it. It is the whole basis of the `if_untouched`
// auto-resolve policy.
//
// Publishing an incident to a page does NOT stamp it: choosing to make an
// outage public is not the same as taking over its narrative, and treating
// it as such turned `if_untouched` into `never` for every hand-published
// entry.
HumanTouchedAt *time.Time `bun:"human_touched_at"`
PublishedAt time.Time `bun:"published_at,notnull,default:current_timestamp"`
ResolvedAt *time.Time `bun:"resolved_at"`
// NotifyWindowStart / NotifyWindowCount implement the per-publication
// subscriber storm cap: at most N fan-out waves per rolling hour. Internal
// bookkeeping — never serialized publicly.
NotifyWindowStart *time.Time `bun:"notify_window_start"`
NotifyWindowCount int `bun:"notify_window_count,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
IncidentPublication is the publication overlay: "this incident is visible on this status page, under this customer-readable title, in this state".
It is deliberately NOT the incident row. The operational `incidents` row carries ack/snooze metadata, auto-generated internal titles and probe diagnostics (`details`), none of which may ever reach a customer. Everything on this struct is safe to render publicly EXCEPT the two notify-window counters, which are internal bookkeeping.
func NewIncidentPublication ¶
func NewIncidentPublication(orgUID, statusPageUID, title string, now time.Time) *IncidentPublication
NewIncidentPublication builds a publication row with a generated UID, opening in the `investigating` state.
func (*IncidentPublication) IsResolved ¶
func (p *IncidentPublication) IsResolved() bool
IsResolved reports whether the publication has been closed.
type IncidentPublicationUpdate ¶
type IncidentPublicationUpdate struct {
PublicTitle *string
PublicState *PublicationState
Severity *string
ClearSeverity bool
HumanTouchedAt *time.Time
ResolvedAt *time.Time
ClearResolvedAt bool
// NotifyWindowStart / NotifyWindowCount are written together by the storm
// cap; a nil NotifyWindowStart with NotifyWindowCount set is not a valid
// combination and is ignored.
NotifyWindowStart *time.Time
NotifyWindowCount *int
}
IncidentPublicationUpdate is the tri-state patch applied to a publication row. A nil pointer leaves the column untouched; a Clear* flag writes NULL.
type IncidentState ¶
type IncidentState int
IncidentState represents the state of an incident.
const ( // IncidentStateActive indicates the incident is ongoing. IncidentStateActive IncidentState = 1 // IncidentStateResolved indicates the incident has been resolved. IncidentStateResolved IncidentState = 2 )
type IncidentUpdate ¶
type IncidentUpdate struct {
Region *string
State *IncidentState
ResolvedAt *time.Time
ResolvedBy *string
ResolutionType *string
EscalatedAt *time.Time
AcknowledgedAt *time.Time
AcknowledgedBy *string
SnoozedUntil *time.Time
SnoozedBy *string
SnoozeReason *string
FailureCount *int
RelapseCount *int
FlapLevel *int
LastReopenedAt *time.Time
Title *string
Description *string
Details *JSONMap
CausedByIncidentUID *string
PagingSuppressed *bool
// Clear* fields set columns to NULL on reopen
ClearResolvedAt bool
ClearResolvedBy bool
ClearResolutionType bool
ClearAcknowledgedAt bool
ClearAcknowledgedBy bool
ClearSnoozedUntil bool
ClearSnoozedBy bool
ClearSnoozeReason bool
ClearCausedByIncidentUID bool
}
IncidentUpdate represents fields that can be updated.
type Integration ¶
type Integration struct {
bun.BaseModel `bun:"table:integrations,alias:integration"`
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
Type ConnectionType `bun:"type,notnull"`
Name string `bun:"name,notnull"`
// No `default:true` on the tag even though the column has one — see the
// StatusPage.AutoPublishDelaySeconds note: it made an integration
// impossible to CREATE disabled (spec 2026-08-30-04). NewIntegration
// supplies the enabled-by-default.
Enabled bool `bun:"enabled,notnull"`
IsDefault bool `bun:"is_default,notnull"`
Settings JSONMap `bun:"settings,type:jsonb,notnull"`
// SettingsPrivate / SettingsPrivateKeys mirror the credential-encryption
// shape used on Check.Config. Tokens, webhook URLs, API keys live here
// as an AES-GCM envelope at rest.
SettingsPrivate *string `bun:"settings_private,type:text,nullzero"`
SettingsPrivateKeys *string `bun:"settings_private_keys,type:text,nullzero"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
// Relations
Organization *Organization `bun:"rel:belongs-to,join:organization_uid=uid"`
}
Integration represents a stored, per-org, credentialed connection to a third-party system — Slack, Discord, email, generic webhook, Freebox, etc. It is the umbrella entity; when an integration can receive notifications (CanNotify) it plays the "channel" role. Backed by the `integrations` table.
func NewIntegration ¶
func NewIntegration(orgUID string, connType ConnectionType, name string) *Integration
NewIntegration creates a new integration with generated UID.
type IntegrationUpdate ¶
type IntegrationUpdate struct {
Name *string
Enabled *bool
IsDefault *bool
Settings *JSONMap
SettingsPrivate *string
SettingsPrivateKeys *string
ClearSettingsPrivate bool
}
IntegrationUpdate represents fields that can be updated.
type JSONMap ¶
JSONMap represents flexible key-value data stored as JSON.
func GetEffectiveSettings ¶
func GetEffectiveSettings(connection *Integration, checkConnection *CheckConnection) JSONMap
GetEffectiveSettings merges connection defaults with check-level overrides. The check-connection settings take precedence over connection defaults.
func ParameterValue ¶
ParameterValue wraps a scalar into the JSON envelope parameters are stored in.
type Job ¶
type Job struct {
UID string `bun:"uid,pk,type:varchar(36)" json:"uid"`
OrganizationUID *string `bun:"organization_uid" json:"organizationUid"`
Type string `bun:"type,notnull" json:"type"`
Config JSONMap `bun:"config,type:jsonb,nullzero" json:"config"`
RetryCount int `bun:"retry_count,notnull" json:"retryCount"`
ScheduledAt time.Time `bun:"scheduled_at,notnull,default:current_timestamp" json:"scheduledAt"`
Status JobStatus `bun:"status,notnull" json:"status"`
Output JSONMap `bun:"output,type:jsonb,nullzero" json:"output"`
PreviousJobUID *string `bun:"previous_job_uid" json:"previousJobUid"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"createdAt"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp" json:"updatedAt"`
DeletedAt *time.Time `bun:"deleted_at" json:"deletedAt,omitempty"`
}
Job represents a background task that can be scheduled and executed.
type JobStatus ¶
type JobStatus string
JobStatus represents the status of a job.
const ( // JobStatusPending indicates the job is waiting to be executed. JobStatusPending JobStatus = "pending" // JobStatusRunning indicates the job is currently executing. JobStatusRunning JobStatus = "running" // JobStatusSuccess indicates the job completed successfully. JobStatusSuccess JobStatus = "success" // JobStatusRetried indicates the job failed and a retry was created. JobStatusRetried JobStatus = "retried" // JobStatusFailed indicates the job failed permanently (no more retries). JobStatusFailed JobStatus = "failed" )
func FinishedJobStatuses ¶
func FinishedJobStatuses() []JobStatus
FinishedJobStatuses returns the terminal job statuses eligible for retention cleanup (jobs_cleanup stage 1 soft-delete). pending/running are deliberately excluded — recovering those is the stuck-job reaper's mandate, not cleanup's. A fresh slice each call keeps callers free to pass it straight into a query.
type JobUpdate ¶
type JobUpdate struct {
Config *JSONMap
RetryCount *int
ScheduledAt *time.Time
Status *JobStatus
Output *JSONMap
PreviousJobUID *string
}
JobUpdate represents fields that can be updated.
type KubernetesPrivateSettings ¶
type KubernetesPrivateSettings struct {
// Token is a bearer token (typically a service-account token) presented to
// the API server.
Token string `json:"token,omitempty"`
// Kubeconfig is a full kubeconfig YAML document that resolves to an API
// server + credentials. Takes precedence over Token when set.
Kubeconfig string `json:"kubeconfig,omitempty"`
}
KubernetesPrivateSettings carries the encrypted secret half of a Kubernetes cluster connection. Exactly one of Token or Kubeconfig is set for a remote connection; both are empty for an in-cluster connection.
func KubernetesPrivateSettingsFromMap ¶
func KubernetesPrivateSettingsFromMap(decrypted map[string]any) *KubernetesPrivateSettings
KubernetesPrivateSettingsFromMap parses the decrypted secret half from a plaintext map (the shape returned by credentials.DecryptForOrg or the plaintext fallback on Settings).
type KubernetesSettings ¶
type KubernetesSettings struct {
// APIServer is the cluster API server URL (e.g. "https://10.0.0.1:6443").
// Empty when InCluster is true.
APIServer string `json:"apiServer,omitempty"`
// CACert is the PEM-encoded cluster CA bundle used to verify the API
// server certificate. Optional; ignored when InsecureSkipTLSVerify is set.
CACert string `json:"caCert,omitempty"`
// InsecureSkipTLSVerify disables API-server certificate verification.
// Use only for clusters with self-signed certs you cannot pin.
InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"`
// InCluster resolves the connection from the pod's mounted service-account
// token (rest.InClusterConfig). Only works when solidping runs inside the
// target cluster; needs no stored secret.
InCluster bool `json:"inCluster,omitempty"`
}
KubernetesSettings is the public (queryable) side of a Kubernetes cluster connection's Settings JSONB. The matching secret — a bearer token or a pasted kubeconfig — lives encrypted in SettingsPrivate under the "token" / "kubeconfig" keys (see KubernetesPrivateSettings). An in-cluster connection (InCluster=true) stores no secret and is resolved via the mounted service account at connect time.
func KubernetesSettingsFromJSONMap ¶
func KubernetesSettingsFromJSONMap(m JSONMap) (*KubernetesSettings, error)
KubernetesSettingsFromJSONMap parses KubernetesSettings from a JSONMap.
func (*KubernetesSettings) ToJSONMap ¶
func (ks *KubernetesSettings) ToJSONMap() (JSONMap, error)
ToJSONMap converts KubernetesSettings to JSONMap for storage.
type Label ¶
type Label struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
Key string `bun:"key,notnull"`
Value string `bun:"value,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
Label represents a key-value pair for categorizing checks.
type LabelSuggestion ¶
LabelSuggestion is one row of an autocomplete query: either a label key (when listing distinct keys) or a label value (when listing distinct values for a given key), together with the number of distinct checks carrying it.
type ListChecksFilter ¶
type ListChecksFilter struct {
Labels map[string]string // key:value pairs for AND filtering
CheckGroupUID *string // filter by check group UID; "none" = ungrouped checks only
Query string // search term for name/slug (case-insensitive substring)
Types []string // optional filter by check type (e.g. ["ssh"]); empty = every type
Internal *string // "true", "false", or "all" — filter by internal status
Statuses []CheckStatus // optional filter by current status (up/down/etc.)
Limit int // max results to return (0 = no limit)
CursorCreatedAt *time.Time // cursor: created_at of last item from previous page
CursorUID *string // cursor: uid of last item from previous page
// SortByGroup opts into display-order pagination (sort=group): group
// sort_order asc, ungrouped last, then created_at DESC / uid DESC within a
// bucket. Off = the default created_at DESC / uid DESC ordering.
SortByGroup bool
// CursorGroupSortKey is the effective group sort key of the last item from
// the previous page — the leading component of the composite sort=group
// cursor. Only set alongside CursorCreatedAt/CursorUID when SortByGroup.
CursorGroupSortKey *int64
// SortByTargetHost opts into the by-host-view pagination (sort=targetHost):
// targetHost sort key ascending (checks with none of host/url/target last),
// then name ascending, then uid ascending as the final tiebreaker.
SortByTargetHost bool
// CursorTargetHostKey and CursorTargetHostName are the leading two
// components of the composite sort=targetHost cursor (the third, uid, reuses
// CursorUID). Only set alongside CursorUID when SortByTargetHost.
CursorTargetHostKey *string
CursorTargetHostName *string
}
ListChecksFilter provides filtering options for listing checks.
type ListEventsFilter ¶
type ListEventsFilter struct {
OrganizationUID string // Required: organization scope
IncidentUID *string // Optional: filter by incident UID
CheckUID *string // Optional: filter by check UID
EventTypes []EventType // Optional: filter by exact event types
// EventTypePrefixes filters by event-type FAMILY: "auth" matches every
// auth.* type. Combined with EventTypes as an OR (either predicate may
// admit a row); empty means "no family restriction".
EventTypePrefixes []string
// ExcludeEventTypePrefixes hides whole families. This is the server-side
// half of "auth events are admin-only": a non-admin caller always carries
// ExcludeEventTypePrefixes=["auth"], so neither an unfiltered listing nor
// an explicit ?type=auth can leak them.
ExcludeEventTypePrefixes []string
ActorType *ActorType // Optional: filter by actor type
// ActorUID filters to the events one user caused (API: actorUserUid).
ActorUID *string
// TargetUID filters to the events about one object, matched against the
// payload's target_uid. Stored in JSON rather than a column because the
// target is polymorphic (a check, a policy, a token, a member…), so this
// is a payload predicate rather than an indexed one.
TargetUID *string
// TargetType filters to one kind of object ("integration", "member", …).
TargetType *string
// TargetSearch is the operator-facing free-text target filter: it matches
// an exact target_uid OR a case-insensitive substring of the target_name
// captured on the event.
//
// Both halves, because an operator has one box and two things they might
// paste into it — the UID from a URL, or the name they remember. A
// UID-only filter behind a box labeled "name or UID" is a promise the
// query silently breaks.
TargetSearch *string
// SourceIP filters to the events that came from one client address.
//
// ADMIN-ONLY at the service layer, for the same reason the column is
// withheld from non-admins: without that gate a viewer who cannot SEE the
// addresses could still use this filter as an oracle — ask for an IP, get
// a non-empty page, and you have confirmed a colleague was working from
// it. A withheld column plus an open filter is not a gate.
SourceIP *string
Since *time.Time // Optional: events created after this time
Until *time.Time // Optional: events created before this time
// Cursor-based pagination
CursorTimestamp *time.Time // Optional: events with created_at < this timestamp
CursorUID *string // Optional: for same timestamp, events with UID < this
Limit int // Optional: pagination limit
}
ListEventsFilter provides filtering options for listing events.
type ListFilesFilter ¶
type ListFilesFilter struct {
Q string
// Topic restricts the listing to attachments with this EXACT topic.
// Mutually exclusive with TopicPrefix in practice; both may be set and
// both are then applied.
Topic string
// TopicPrefix restricts the listing to attachments whose topic starts with
// this string — the entity-scoped form, e.g. `incidents/<uid>/`.
TopicPrefix string
Offset int
Limit int
}
ListFilesFilter provides filtering options for listing files.
type ListIncidentPublicationsFilter ¶
type ListIncidentPublicationsFilter struct {
OrganizationUID string
StatusPageUID string
IncidentUID string
// State filters to a single public state when non-empty.
State string
// ActiveOnly restricts to publications that are not resolved.
ActiveOnly bool
Limit int
Offset int
}
ListIncidentPublicationsFilter narrows a publication listing.
type ListIncidentsFilter ¶
type ListIncidentsFilter struct {
OrganizationUID string // Required: organization scope
CheckUIDs []string // Optional: filter by check UIDs
CheckGroupUID string // Optional: filter to group incidents on this group
MemberCheckUID string // Optional: incidents that contain this check (per-check or group member)
States []IncidentState // Optional: filter by states (active, resolved)
// Kinds restricts to these incident kinds. EMPTY MEANS ALL, on purpose:
// the dashboard's incident list is meant to show burn alerts alongside
// check outages. Callers that compute DOWNTIME from incidents (availability,
// SLO status, uptime reports) pass IncidentKindCheck, or a burn alert would
// count as downtime against the very objective that produced it.
Kinds []string
Since *time.Time // Optional: incidents started after this time
Until *time.Time // Optional: incidents started before this time
HideSuppressed bool // Optional: hide rolled-up (paging-suppressed) incidents
CausedByUID string // Optional: only incidents whose caused_by_incident_uid equals this
// AckedOnly returns only incidents with non-NULL acknowledged_at (and
// expired snoozes, since snoozed-but-expired is just acked again). Set by
// the handler when ?state=acked is requested.
AckedOnly bool
// SnoozedOnly returns only incidents currently snoozed (snoozed_until in
// the future). Set by the handler when ?state=snoozed is requested.
SnoozedOnly bool
// Cursor-based pagination
CursorTimestamp *time.Time // Optional: incidents with started_at < this timestamp
CursorUID *string // Optional: for same timestamp, incidents with UID < this
Limit int // Optional: pagination limit
}
ListIncidentsFilter provides filtering options for listing incidents.
type ListIntegrationsFilter ¶
type ListIntegrationsFilter struct {
OrganizationUID string
Type *ConnectionType
Enabled *bool
}
ListIntegrationsFilter represents filter options for listing integrations.
type ListMaintenanceWindowsFilter ¶
type ListMaintenanceWindowsFilter struct {
Status string // "active", "upcoming", "past", or "" for all
Limit int // max results (0 = no limit)
}
ListMaintenanceWindowsFilter provides filtering options for listing maintenance windows.
type ListMembershipRequestsFilter ¶
type ListMembershipRequestsFilter struct {
OrganizationUID string
UserUID string
Status MembershipRequestStatus
Limit int
Offset int
}
ListMembershipRequestsFilter narrows a list query.
type ListOrgEntitlementAuditsFilter ¶
ListOrgEntitlementAuditsFilter narrows an audit list query.
type ListResultsFilter ¶
type ListResultsFilter struct {
OrganizationUID string // Required: organization scope
CheckUIDs []string // Optional: filter by multiple check UIDs
CheckTypes []string // Optional: filter by check types (requires join with checks table)
Regions []string // Optional: filter by multiple regions
PeriodTypes []string // Optional: filter by multiple period_types ('raw', 'hour', 'day', 'month')
Statuses []int // Optional: filter by multiple status integers (inclusion)
// ExcludeStatuses drops rows whose status is in this set (NULL status is
// never excluded). Used by aggregation work-discovery to skip buckets whose
// only rows are lifecycle markers (created/running), which would otherwise
// re-aggregate into degenerate rollups forever (poison-pill loop, spec
// 2026-07-11-16). Applied independently of Statuses.
ExcludeStatuses []int
// RequireCheckExists, when true, drops rows whose check_uid no longer has a
// row in `checks` (`check_uid IN (SELECT uid FROM checks)`). Soft-deleted
// checks still have a row and keep their history — only rows orphaned by a
// hard delete are excluded. Used by aggregation work-discovery so a single
// FK-orphan raw row can't become a deterministic poison pill that fails the
// rollup INSERT and permanently halts the org's aggregation (spec
// 2026-07-12-01 §2). A no-op on Postgres by FK construction; implemented on
// both backends for parity and defense in depth.
RequireCheckExists bool
PeriodStartAfter *time.Time // Optional: filter period_start >= this value
// Optional: filter period_start < this value (filters by period_start, not period_end)
PeriodEndBefore *time.Time
// Cursor-based pagination
CursorTimestamp *time.Time // Optional: results with period_start < this timestamp
CursorUID *string // Optional: for same timestamp, results with UID < this
// Limit
Limit int // Optional: pagination limit
// Include check info (for joining with checks table to get slug/name)
IncludeCheckInfo bool // Optional: whether to join with checks table
// SkipBlobs, when true, drops the two JSON blob columns (`metrics`,
// `output`) from the SELECT projection: the scanned rows come back with
// Metrics and Output nil regardless of what is stored. Only for consumers
// that need status/counts/duration and never render a blob (uptime bars,
// badges, status-page recent results) — it trims per-request I/O on the
// largest table in the system (spec 2026-07-24-02 §5). Default (false)
// keeps the full row, so every other reader is unaffected.
SkipBlobs bool
}
ListResultsFilter provides filtering options for listing results.
type ListResultsResponse ¶
type ListResultsResponse struct {
Results []*Result // The result records
}
ListResultsResponse wraps a page of results. There is deliberately no total count, next-cursor, or has-more field here: `results` is the largest table in the system, so this endpoint is cursor-paginated and the DB layer never computes any of the three (spec 2026-08-18-04). The service layer derives its own cursor/has-more by over-fetching one extra row (internal/handlers/results/service.go), which is why those fields would be dead weight on this struct even if the DB layer did populate them.
type ListSLOsFilter ¶
type ListSLOsFilter struct {
// CheckUID, when set, restricts to SLOs scoped directly to that check.
CheckUID string
// EnabledOnly restricts to enabled SLOs.
EnabledOnly bool
Limit int
}
ListSLOsFilter provides filtering options for listing SLOs.
type ListSeveritiesFilter ¶
type ListSeveritiesFilter struct {
OrganizationUID string
}
ListSeveritiesFilter narrows a list query.
type ListSupportThreadsFilter ¶
ListSupportThreadsFilter configures the thread listing.
type MSTeamsBotSettings ¶
type MSTeamsBotSettings struct {
TenantID string `json:"tenant_id"`
TenantName string `json:"tenant_name,omitempty"`
// BotID is the bot's own Bot Framework user id inside this tenant
// (`28:<app-id>`), used to recognize "the member added is us".
BotID string `json:"bot_id,omitempty"`
// AppID records which Entra app performed the install, so a credential
// rotation that changes the app id is visible rather than silent.
AppID string `json:"app_id,omitempty"`
// ServiceURL is the tenant's regional Bot Connector base URL, captured at
// install and refreshed on every inbound activity.
ServiceURL string `json:"service_url,omitempty"`
// ChannelID / ChannelName / TeamID are the default notification
// destination — the Teams counterpart of SlackSettings.ChannelID.
ChannelID string `json:"channel_id,omitempty"`
ChannelName string `json:"channel_name,omitempty"`
TeamID string `json:"team_id,omitempty"`
DisplayName string `json:"display_name,omitempty"`
InstalledByUserID string `json:"installed_by_user_id,omitempty"`
// UninstalledAt is set (RFC3339) when the tenant removes the app. The row
// is kept so the dashboard can render "uninstalled — reinstall to resume"
// instead of the integration silently vanishing.
UninstalledAt string `json:"uninstalled_at,omitempty"`
Destinations []MSTeamsDestination `json:"destinations,omitempty"`
}
MSTeamsBotSettings represents the settings JSONB of a `msteams-bot` connection. It is the Teams analog of SlackSettings: TenantID replaces TeamID as the workspace identity, and Destinations holds the conversation references captured when the bot was added to a team/channel (Teams has no "list all channels" API for a bot, so destinations are accumulated from install/conversationUpdate activities rather than fetched on demand).
No credential lives here: the Entra app id/secret are instance-level system config (SaaS: SolidPing's multi-tenant app; self-hosted: the operator's own), so a stolen settings blob grants nothing. `app_secret` is still registered in credentials.ConnectionSecretFields as defense in depth for any future per-connection override.
func MSTeamsBotSettingsFromJSONMap ¶
func MSTeamsBotSettingsFromJSONMap(m JSONMap) (*MSTeamsBotSettings, error)
MSTeamsBotSettingsFromJSONMap parses MSTeamsBotSettings from a JSONMap.
func (*MSTeamsBotSettings) FindDestination ¶
func (s *MSTeamsBotSettings) FindDestination(conversationID string) *MSTeamsDestination
FindDestination returns the captured conversation reference with this id.
func (*MSTeamsBotSettings) HasDestination ¶
func (s *MSTeamsBotSettings) HasDestination(conversationID string) bool
HasDestination reports whether conversationID is one of the conversation references this connection actually captured from Bot Framework.
This is the authorization rule for every destination selection, wherever it is made: the dashboard PATCH, the per-check override, and the in-band `config default-channel` command all funnel through it. A Teams conversation id is discoverable (it appears in "Get link to channel" URLs), so treating one as usable just because a client named it would let an org post into a channel belonging to a different tenant — the same "asserted vs proven identifier" mistake that made tenant_id exploitable.
func (*MSTeamsBotSettings) ToJSONMap ¶
func (s *MSTeamsBotSettings) ToJSONMap() (JSONMap, error)
ToJSONMap converts MSTeamsBotSettings to JSONMap for storage.
type MSTeamsDestination ¶
type MSTeamsDestination struct {
ID string `json:"id"`
Name string `json:"name"`
TeamID string `json:"team_id,omitempty"`
TeamName string `json:"team_name,omitempty"`
ServiceURL string `json:"service_url,omitempty"`
// Type is "channel" today. Personal-scope DMs are phase 2 and would add
// "personal" here without changing the shape.
Type string `json:"type,omitempty"`
}
MSTeamsDestination is a single captured Bot Framework conversation reference — the Teams equivalent of a Slack channel in the destinations picker. `ID` is the Bot Framework conversation id (for a channel-scoped install this is the channel's thread id), `ServiceURL` is the regional Bot Connector base URL that conversation must be addressed through, and `TeamID` / `TeamName` carry the owning team so the dashboard can group channels per team.
type MSTeamsSettings ¶
type MSTeamsSettings struct {
WebhookURL string `json:"webhook_url"`
}
MSTeamsSettings represents Microsoft Teams-specific settings stored in the Settings JSONB. WebhookURL is the Teams Workflow ("When a Teams webhook request is received") URL — the legacy Office 365 Connector format is not supported. Uses webhook_url (matching Discord, see DiscordSettings above) end-to-end on both the frontend form and this Go struct tag; unlike googlechat/mattermost, there is no key mismatch here by design.
func MSTeamsSettingsFromJSONMap ¶
func MSTeamsSettingsFromJSONMap(m JSONMap) (*MSTeamsSettings, error)
MSTeamsSettingsFromJSONMap parses MSTeamsSettings from a JSONMap.
func (*MSTeamsSettings) ToJSONMap ¶
func (ms *MSTeamsSettings) ToJSONMap() (JSONMap, error)
ToJSONMap converts MSTeamsSettings to JSONMap for storage.
type MaintenanceWindow ¶
type MaintenanceWindow struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
Title string `bun:"title,notnull"`
Description *string `bun:"description"`
StartAt time.Time `bun:"start_at,notnull"`
EndAt time.Time `bun:"end_at,notnull"`
Recurrence string `bun:"recurrence,notnull"`
RecurrenceEnd *time.Time `bun:"recurrence_end"`
CreatedBy *string `bun:"created_by"`
CreatedAt time.Time `bun:"created_at,notnull"`
UpdatedAt time.Time `bun:"updated_at,notnull"`
DeletedAt *time.Time `bun:"deleted_at"`
}
MaintenanceWindow represents a scheduled maintenance window for an organization.
func NewMaintenanceWindow ¶
func NewMaintenanceWindow(orgUID, title string, startAt, endAt time.Time) *MaintenanceWindow
NewMaintenanceWindow creates a new maintenance window with generated UID.
type MaintenanceWindowCheck ¶
type MaintenanceWindowCheck struct {
UID string `bun:"uid,pk,type:varchar(36)"`
MaintenanceWindowUID string `bun:"maintenance_window_uid,notnull"`
CheckUID *string `bun:"check_uid"`
CheckGroupUID *string `bun:"check_group_uid"`
CreatedAt time.Time `bun:"created_at,notnull"`
}
MaintenanceWindowCheck represents the association between a maintenance window and a check or check group.
type MaintenanceWindowUpdate ¶
type MaintenanceWindowUpdate struct {
Title *string
Description *string
StartAt *time.Time
EndAt *time.Time
Recurrence *string
RecurrenceEnd *time.Time
}
MaintenanceWindowUpdate represents fields that can be updated on a maintenance window.
type MemberRole ¶
type MemberRole string
MemberRole represents a user's role in an organization.
const ( MemberRoleOwner MemberRole = "owner" MemberRoleAdmin MemberRole = "admin" MemberRoleUser MemberRole = "user" MemberRoleViewer MemberRole = "viewer" )
Member roles, from most to least privileged. The ordering is meaningful: owner > admin > user > viewer. Use MemberRole.AtLeast rather than equality whenever a call site gates on "at least this much privilege" — an owner must pass every admin gate.
func (MemberRole) AtLeast ¶
func (r MemberRole) AtLeast(minRole MemberRole) bool
AtLeast reports whether the role carries at least the privilege of min. An unknown role never satisfies any gate; an unknown min is never satisfied either (it ranks 0 but a role must still be valid to pass).
func (MemberRole) IsValid ¶
func (r MemberRole) IsValid() bool
IsValid reports whether the role is one of the known member roles.
func (MemberRole) Rank ¶
func (r MemberRole) Rank() int
Rank returns the role's privilege rank. Unknown roles rank below every valid role.
type MembershipRequest ¶
type MembershipRequest struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
UserUID string `bun:"user_uid,notnull"`
Message *string `bun:"message"`
Status MembershipRequestStatus `bun:"status,notnull"`
DecisionReason *string `bun:"decision_reason"`
DecidedAt *time.Time `bun:"decided_at"`
DecidedByUID *string `bun:"decided_by_uid"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
Organization *Organization `bun:"rel:belongs-to,join:organization_uid=uid"`
User *User `bun:"rel:belongs-to,join:user_uid=uid"`
DecidedBy *User `bun:"rel:belongs-to,join:decided_by_uid=uid"`
}
MembershipRequest represents a user's ask to join an organization.
func NewMembershipRequest ¶
func NewMembershipRequest(orgUID, userUID string, message *string) *MembershipRequest
NewMembershipRequest creates a new pending request with a generated UID.
type MembershipRequestStatus ¶
type MembershipRequestStatus string
MembershipRequestStatus is the lifecycle state of a membership request.
const ( MembershipRequestStatusPending MembershipRequestStatus = "pending" MembershipRequestStatusApproved MembershipRequestStatus = "approved" MembershipRequestStatusRejected MembershipRequestStatus = "rejected" MembershipRequestStatusCancelled MembershipRequestStatus = "canceled" )
Membership request statuses.
type OAuthClient ¶
type OAuthClient struct {
bun.BaseModel `bun:"table:oauth_clients,alias:oauth_client"`
UID string `bun:"uid,pk,type:varchar(36)"`
ClientID string `bun:"client_id,notnull"`
SecretHash *string `bun:"secret_hash"`
ClientName string `bun:"client_name"`
RedirectURIs []string `bun:"redirect_uris,type:jsonb,nullzero"`
GrantTypes []string `bun:"grant_types,type:jsonb,nullzero"`
Scopes []string `bun:"scopes,type:jsonb,nullzero"`
IsPublic bool `bun:"is_public,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}
OAuthClient is a registered OAuth 2.1 client for the MCP authorization server. Native MCP clients (Claude Desktop, mcp-remote) register dynamically (RFC 7591) as public clients (no secret) using PKCE + loopback redirects; confidential clients store a hashed secret. Redirect URIs are stored as a JSON array and validated on every /authorize.
func NewOAuthClient ¶
func NewOAuthClient(clientID string) *OAuthClient
NewOAuthClient builds a new client row with a generated UID.
type Occurrence ¶
Occurrence is one concrete activation of a (possibly recurring) maintenance window.
func NextOccurrences ¶
func NextOccurrences(window *MaintenanceWindow, from time.Time, n int) []Occurrence
NextOccurrences returns up to n occurrences whose end is at/after from, in chronological order, honoring RecurrenceEnd. For recurrence "none" it returns the single window when not yet past, else an empty slice. The currently-active occurrence (if any) is included as the first entry.
type OnCallSchedule ¶
type OnCallSchedule struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
Name string `bun:"name,notnull"`
Description *string `bun:"description"`
Timezone string `bun:"timezone,notnull"`
RotationType RotationType `bun:"rotation_type,notnull"`
HandoffTime string `bun:"handoff_time,notnull"` // HH:MM in schedule timezone
HandoffWeekday *int `bun:"handoff_weekday"` // 0–6 (Mon=0); required for weekly
StartAt time.Time `bun:"start_at,notnull"` // First handoff in the rotation cycle
ICalSecret *string `bun:"ical_secret"` // NULL = feed disabled
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
OnCallSchedule is a rotation: a list of users plus a cadence and timezone that, together with a moment in time, resolves to one user (the "currently on call"). The schedule itself does not page anyone — escalation policies (separate spec) consume schedules at fan-out time.
func NewOnCallSchedule ¶
func NewOnCallSchedule(orgUID, name, timezone string, rotation RotationType) *OnCallSchedule
NewOnCallSchedule builds a schedule with a fresh UID; caller fills the remaining fields.
type OnCallScheduleOverride ¶
type OnCallScheduleOverride struct {
UID string `bun:"uid,pk,type:varchar(36)"`
ScheduleUID string `bun:"schedule_uid,notnull"`
UserUID string `bun:"user_uid,notnull"`
StartAt time.Time `bun:"start_at,notnull"` // inclusive
EndAt time.Time `bun:"end_at,notnull"` // exclusive
Reason *string `bun:"reason"`
CreatedByUID *string `bun:"created_by_uid"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
}
OnCallScheduleOverride is a time-bounded replacement of the rotation's next on-call user. Overlapping overrides resolve to the most recently created (documented behavior, not validated at write time).
func NewOnCallScheduleOverride ¶
func NewOnCallScheduleOverride( scheduleUID, userUID string, startAt, endAt time.Time, ) *OnCallScheduleOverride
NewOnCallScheduleOverride builds an override row with a fresh UID.
type OnCallScheduleUpdate ¶
type OnCallScheduleUpdate struct {
Name *string
Description *string
Timezone *string
RotationType *RotationType
HandoffTime *string
HandoffWeekday *int
StartAt *time.Time
ICalSecret *string
ClearDescription bool
ClearHandoffWeekday bool
ClearICalSecret bool
}
OnCallScheduleUpdate captures the writable fields of a schedule. Pointer semantics: nil = unchanged, non-nil = set.
type OnCallScheduleUser ¶
type OnCallScheduleUser struct {
UID string `bun:"uid,pk,type:varchar(36)"`
ScheduleUID string `bun:"schedule_uid,notnull"`
UserUID string `bun:"user_uid,notnull"`
Position int `bun:"position,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}
OnCallScheduleUser is one row in a schedule's ordered roster.
func NewOnCallScheduleUser ¶
func NewOnCallScheduleUser(scheduleUID, userUID string, position int) *OnCallScheduleUser
NewOnCallScheduleUser builds a roster entry with a fresh UID.
type OrgEntitlementAudit ¶
type OrgEntitlementAudit struct {
UID string `bun:"uid,pk,type:varchar(36)" json:"uid"`
OrganizationUID string `bun:"organization_uid,notnull" json:"organizationUid"`
Source string `bun:"source,notnull" json:"source"`
Actor string `bun:"actor,notnull" json:"actor"`
BeforeSnapshot JSONMap `bun:"before_snapshot,type:jsonb,nullzero" json:"beforeSnapshot,omitempty"`
AfterSnapshot JSONMap `bun:"after_snapshot,type:jsonb,notnull" json:"afterSnapshot"`
Reason *string `bun:"reason" json:"reason,omitempty"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp" json:"createdAt"`
}
OrgEntitlementAudit records one write to org_entitlements. The before snapshot is nil on the first row for an org; after is always populated.
The JSON tags are load-bearing: this model is serialized straight onto the audit-listing endpoint, and without them Go would emit Go field names while the OpenAPI spec (and the client generated from it) promise camelCase.
func NewOrgEntitlementAudit ¶
func NewOrgEntitlementAudit( orgUID, source, actor string, before JSONMap, after JSONMap, reason *string, ) *OrgEntitlementAudit
NewOrgEntitlementAudit builds a fresh audit row.
type OrgEntitlements ¶
type OrgEntitlements struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull,unique"`
Payload EntitlementsPayload `bun:"payload,type:jsonb,notnull"`
ExternalRef *string `bun:"external_ref"`
ExpiresAt *time.Time `bun:"expires_at"`
LastSyncedAt *time.Time `bun:"last_synced_at"`
Metadata JSONMap `bun:"metadata,type:jsonb,nullzero"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}
OrgEntitlements is one row per org. Limits, features, and source live inside Payload; absence inside the payload means "use the in-code default" (resolution happens in the entitlements service, never stored). ExternalRef / ExpiresAt / LastSyncedAt are kept as columns because they are queried or indexed at the SQL level.
func NewOrgEntitlements ¶
func NewOrgEntitlements(orgUID string, source EntitlementSource) *OrgEntitlements
NewOrgEntitlements builds a fresh row with the given source. The payload is initialized with the current schema version and empty limits/features so the resolver merges in defaults.
type OrgUsageCounter ¶
type OrgUsageCounter struct {
bun.BaseModel `bun:"table:org_usage_counters"`
OrganizationUID string `bun:"organization_uid,pk"`
Kind string `bun:"kind,pk"`
PeriodStart string `bun:"period_start,pk"`
Count int `bun:"count,notnull"`
}
OrgUsageCounter is a persistent per-org, per-kind, per-period counter backing the SMS/voice/WhatsApp monthly quotas and the daily rate-limited-skip tally. PeriodStart is an ISO date string ("2006-01-02") whose granularity is decided by the kind — the first day of the UTC month for the monthly quotas, the UTC day itself for UsageCounterKindCheckRateLimited. It is stored in a date column on PostgreSQL and text on SQLite.
type Organization ¶
type Organization struct {
UID string `bun:"uid,pk,type:varchar(36)"`
Slug string `bun:"slug,notnull"`
Name string `bun:"name"`
// DefaultEscalationPolicyUID is the org-wide fallback escalation policy for
// checks that resolve to no policy of their own (check → group → org default
// → none). NULL = no org default (legacy behavior). FK `on delete set null`.
DefaultEscalationPolicyUID *string `bun:"default_escalation_policy_uid"`
// LogoURL is the organization's logo: either an external http(s) URL, or
// "/pub/assets/<file uid>" for a logo uploaded through the files
// subsystem. NULL means "no logo", and every surface falls back to the
// product default.
LogoURL *string `bun:"logo_url"`
// LogoFileUID points at the uploaded file backing LogoURL. It is NULL for
// external-URL logos. Replacing an uploaded logo retires the previous blob
// through it. It is NO LONGER an authorization input: since spec
// 2026-08-22-03 the unsigned public route reads the FILE's own attachment
// topic (`organizations/<uid>/logo`) and serves live rows only.
LogoFileUID *string `bun:"logo_file_uid"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
Organization represents a tenant in the system.
func NewOrganization ¶
func NewOrganization(slug, name string) *Organization
NewOrganization creates a new organization with generated UID.
type OrganizationMember ¶
type OrganizationMember struct {
UID string `bun:"uid,pk,type:varchar(36)"`
UserUID string `bun:"user_uid,notnull"`
OrganizationUID string `bun:"organization_uid,notnull"`
Role MemberRole `bun:"role,notnull"`
InvitedByUID *string `bun:"invited_by_uid"`
InvitedAt *time.Time `bun:"invited_at"`
JoinedAt *time.Time `bun:"joined_at"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
// Relations (for eager loading)
User *User `bun:"rel:belongs-to,join:user_uid=uid"`
Organization *Organization `bun:"rel:belongs-to,join:organization_uid=uid"`
InvitedBy *User `bun:"rel:belongs-to,join:invited_by_uid=uid"`
}
OrganizationMember links a user to an organization with a role.
func NewOrganizationMember ¶
func NewOrganizationMember(orgUID, userUID string, role MemberRole) *OrganizationMember
NewOrganizationMember creates a new membership with generated UID.
type OrganizationMemberUpdate ¶
type OrganizationMemberUpdate struct {
Role *MemberRole
JoinedAt *time.Time
}
OrganizationMemberUpdate represents fields that can be updated.
type OrganizationPreviousSlug ¶
type OrganizationPreviousSlug struct {
bun.BaseModel `bun:"table:organization_previous_slugs,alias:org_previous_slug"`
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
Slug string `bun:"slug,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
OrganizationPreviousSlug is a slug an organization used to answer on before it was renamed. Resolution always prefers a live organizations.slug, so an alias can never shadow an existing org; on a miss the alias resolves and the request is redirected to the organization's current slug.
The alias is released (soft-deleted) as soon as another organization claims the slug, which is the only guarantee boundary the UI promises. Deleted orgs are deliberately unreachable through aliases (spec 2026-08-08-11): the lookup joins organizations and requires deleted_at IS NULL.
func NewOrganizationPreviousSlug ¶
func NewOrganizationPreviousSlug(orgUID, slug string) *OrganizationPreviousSlug
NewOrganizationPreviousSlug creates an alias row with a generated UID.
type OrganizationProvider ¶
type OrganizationProvider struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
ProviderType ProviderType `bun:"provider_type,notnull"`
ProviderID string `bun:"provider_id,notnull"` // e.g., Slack Team ID T0123456789
ProviderName string `bun:"provider_name"` // e.g., "Acme Corp Slack Workspace"
Metadata JSONMap `bun:"metadata,type:jsonb,nullzero"`
// MetadataPrivate / MetadataPrivateKeys mirror the credential-encryption
// shape used on Check.Config — OAuth client secrets and similar live
// here as an AES-GCM envelope at rest.
MetadataPrivate *string `bun:"metadata_private,type:text,nullzero"`
MetadataPrivateKeys *string `bun:"metadata_private_keys,type:text,nullzero"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
// Relations (for eager loading)
Organization *Organization `bun:"rel:belongs-to,join:organization_uid=uid"`
}
OrganizationProvider links an organization to an external provider identity. This is the single source of truth for org↔provider mapping (e.g., Slack team, Google Workspace).
func NewOrganizationProvider ¶
func NewOrganizationProvider(orgUID string, providerType ProviderType, providerID string) *OrganizationProvider
NewOrganizationProvider creates a new organization provider with generated UID.
type OrganizationProviderUpdate ¶
type OrganizationProviderUpdate struct {
ProviderName *string
Metadata *JSONMap
MetadataPrivate *string
MetadataPrivateKeys *string
ClearMetadataPrivate bool
}
OrganizationProviderUpdate represents fields that can be updated.
type OrganizationUpdate ¶
type OrganizationUpdate struct {
Slug *string
Name *string
// DefaultEscalationPolicyUID sets the org-wide default escalation policy.
// ClearDefaultEscalationPolicyUID takes precedence and clears it to NULL
// (mirrors the check-group clear-flag pattern).
DefaultEscalationPolicyUID *string
ClearDefaultEscalationPolicyUID bool
// LogoURL / LogoFileUID follow the same set-or-clear pattern: the Clear*
// flag wins and writes NULL, a non-nil pointer writes the value, and nil
// with no flag leaves the column untouched (PATCH semantics).
LogoURL *string
ClearLogoURL bool
LogoFileUID *string
ClearLogoFileUID bool
}
OrganizationUpdate represents fields that can be updated.
type PageResourceStatus ¶
type PageResourceStatus struct {
Status CheckStatus
InMaintenance bool
}
PageResourceStatus is the minimal per-resource input RollupPageStatus needs: the resource's live check status (a check's own CheckStatus, or a check-group resource's already rolled-up status via RollupGroupStatus) and whether it is currently inside an active maintenance window.
type PageStatus ¶
type PageStatus string
PageStatus is a status page's page-level rollup status (spec 2026-08-08-05). It is a distinct vocabulary from CheckStatus: it adds "maintenance" (no individual check has this concept) and gives "unknown" a real, honest meaning instead of silently collapsing into "operational".
const ( PageStatusOperational PageStatus = "operational" PageStatusDegraded PageStatus = "degraded" PageStatusDown PageStatus = "down" PageStatusMaintenance PageStatus = "maintenance" PageStatusUnknown PageStatus = "unknown" )
The page-level rollup vocabulary. Kept as string constants (rather than an int enum like CheckStatus) because this value is only ever produced at the read boundary and serialized straight onto the public wire — there is no storage representation to keep compact.
type PageStatusCounts ¶
PageStatusCounts tallies how many resources landed in each PageStatus category. Per-resource classification applies the same maintenance-masking rule as the overall rollup: a resource in maintenance is counted as Maintenance regardless of its underlying check status.
type Parameter ¶
type Parameter struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID *string `bun:"organization_uid"`
Key string `bun:"key,notnull"`
Value JSONMap `bun:"value,type:jsonb,notnull"`
Secret *bool `bun:"secret"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
Parameter represents a key-value configuration. When OrganizationUID is nil, this is a system-wide parameter.
func NewParameter ¶
NewParameter creates a new organization-scoped parameter with generated UID.
type ParameterUpdate ¶
ParameterUpdate represents fields that can be updated.
type PeriodTierSide ¶
type PeriodTierSide int
PeriodTierSide describes which side of the raw/rollup split a requested set of period types sits on. It exists because BOTH useful indexes on `results` are partial and split on exactly that predicate (`results_raw_idx WHERE period_type = 'raw'`, `results_aggregated_idx WHERE period_type != 'raw'`): a query can only ride one of them if its WHERE clause implies the index's own predicate. See PeriodTypesTierSide.
const ( // PeriodTierMixed names both raw and at least one rollup tier, so neither // partial index is eligible and the query can only be a full scan. Never // issue one — split it into one query per side (spec 2026-08-22-04). PeriodTierMixed PeriodTierSide = iota // PeriodTierRaw names only raw. PeriodTierRaw // PeriodTierRollup names only aggregated tiers (hour/day/month). PeriodTierRollup )
func PeriodTypesTierSide ¶
func PeriodTypesTierSide(periodTypes []string) PeriodTierSide
PeriodTypesTierSide reports which side of the raw/rollup index split the given period types sit on. An empty list is mixed: it constrains nothing, so neither partial predicate is implied.
type ProviderType ¶
type ProviderType string
ProviderType represents an external auth provider type.
const ( ProviderTypeGoogle ProviderType = "google" ProviderTypeGitHub ProviderType = "github" ProviderTypeGitLab ProviderType = "gitlab" ProviderTypeMicrosoft ProviderType = "microsoft" ProviderTypeTwitter ProviderType = "twitter" ProviderTypeSlack ProviderType = "slack" ProviderTypeDiscord ProviderType = "discord" ProviderTypeSAML ProviderType = "saml" ProviderTypeOIDC ProviderType = "oidc" // ProviderTypeLDAP identifies a user auto-provisioned or linked via an // LDAP/Active Directory bind (spec 2026-07-08-08, part 3). Users linked // this way always have a nil User.PasswordHash — see // Service.findOrCreateLDAPUser in internal/handlers/auth/ldap_service.go. ProviderTypeLDAP ProviderType = "ldap" )
Provider types.
type PublicationSeverity ¶
type PublicationSeverity string
PublicationSeverity is the public badge severity. It is display-only: it never routes, pages, or gates anything. Empty/NULL means "no badge".
const ( // PublicationSeverityMinor is a limited-impact issue. PublicationSeverityMinor PublicationSeverity = "minor" // PublicationSeverityMajor is a broad-impact issue. PublicationSeverityMajor PublicationSeverity = "major" // PublicationSeverityCritical is a full outage. PublicationSeverityCritical PublicationSeverity = "critical" )
PublicationSeverity values.
func (PublicationSeverity) IsValid ¶
func (s PublicationSeverity) IsValid() bool
IsValid reports whether the severity is one of the three recognized values.
type PublicationState ¶
type PublicationState string
PublicationState is the customer-facing lifecycle state of an incident publication. It is deliberately a SEPARATE vocabulary from models.IncidentState: the operational incident is active/resolved, while the public narrative walks investigating → identified → monitoring → resolved (the StatusUpdateKind vocabulary customers already see on the timeline).
const ( // PublicationStateInvestigating is the opening state of every publication. PublicationStateInvestigating PublicationState = "investigating" // PublicationStateIdentified means the cause is known, work is ongoing. PublicationStateIdentified PublicationState = "identified" // PublicationStateMonitoring means a fix is in place and being watched. PublicationStateMonitoring PublicationState = "monitoring" // PublicationStateResolved closes the publication. PublicationStateResolved PublicationState = "resolved" )
PublicationState values.
func (PublicationState) IsValid ¶
func (s PublicationState) IsValid() bool
IsValid reports whether the state is one of the four recognized values.
func (PublicationState) UpdateKind ¶
func (s PublicationState) UpdateKind() StatusUpdateKind
UpdateKind maps a publication state onto the StatusUpdateKind used for the narrative row posted alongside a state change. The two vocabularies are intentionally identical in wording so a reader never sees the timeline and the incident header disagree.
type ReapAbandonedResultsOutcome ¶
type ReapAbandonedResultsOutcome struct {
// Candidates is the number of raw rows found sitting in ResultStatusCreated
// (see abandonedResultLifecycleStatuses), before threshold filtering.
Candidates int
// Reaped is the number of those candidates that were past
// AbandonedResultThreshold for their check and got finalized this sweep.
Reaped int
}
ReapAbandonedResultsOutcome reports what one abandoned-result reaper sweep did (see db.Service.ReapAbandonedResults).
type RecentResultsPerCheckFilter ¶
type RecentResultsPerCheckFilter struct {
// OrganizationUID scopes every branch. Required.
OrganizationUID string
// CheckUIDs are the checks to fetch for. An empty list yields no rows.
CheckUIDs []string
// Tiers are the tier-aligned branches, UNION ALLed together. Required, and
// each one must be single-sided.
Tiers []RecentResultsTier
// PerCheckLimits is the per-check row budget, per branch. A check absent
// from the map uses DefaultPerCheckLimit.
PerCheckLimits map[string]int
// DefaultPerCheckLimit is the budget for a check with no explicit entry.
// Required (> 0) — an unbounded branch defeats the whole point.
DefaultPerCheckLimit int
}
RecentResultsPerCheckFilter describes a "newest N rows per check, per tier" fetch. It is deliberately NOT expressible as a ListResultsFilter: that is a generic filtered list whose Limit is global, and a global limit over several checks is exactly the over-fetch-and-discard this query shape exists to remove (spec 2026-08-22-05).
The budget is per check rather than one number for the batch because the caller sizes it from the check's own region fan-out: a 3-region check needs three times the rows of a single-region one to fill the same per-region chart, and a global figure can only be the max of those — which over-fetches for every other check on the page.
func (*RecentResultsPerCheckFilter) LimitFor ¶
func (f *RecentResultsPerCheckFilter) LimitFor(checkUID string) int
LimitFor returns the per-branch row budget for one check.
func (*RecentResultsPerCheckFilter) Validate ¶
func (f *RecentResultsPerCheckFilter) Validate() error
Validate rejects a filter no dialect may execute. The mixed-tier rule is the important one: see ErrRecentResultsMixedTier.
type RecentResultsTier ¶
type RecentResultsTier struct {
// PeriodTypes are the tiers this branch reads. They MUST all sit on one
// side of the raw/rollup split (see PeriodTypesTierSide) — the
// implementation restates that side as an explicit predicate so both
// Postgres and SQLite can use the matching partial index.
PeriodTypes []string
// Since is this branch's inclusive period_start lower bound.
Since time.Time
}
RecentResultsTier is ONE tier-aligned branch of a RecentResultsPerCheck fetch: which aggregation tiers to read, and how far back to read them.
Each branch gets its own lower bound because the tiers live in different windows: raw only exists for the configured raw retention (~24 h), while the rollups reach back months. One shared bound would either truncate the rollups or make the raw branch scan far past where raw rows can exist.
type ReportSchedule ¶
type ReportSchedule struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
Name string `bun:"name,notnull"`
Frequency string `bun:"frequency,notnull"`
Timezone string `bun:"timezone,notnull"`
// Recipients is PII, held to the same bar as status-page subscriber
// addresses: never logged, never emitted into events, only ever read back
// to the org's own admins.
Recipients []string `bun:"recipients,type:jsonb,nullzero"`
// CheckUIDs / CheckGroupUIDs scope the digest. Both empty means org-wide.
CheckUIDs []string `bun:"check_uids,type:jsonb,nullzero"`
CheckGroupUIDs []string `bun:"check_group_uids,type:jsonb,nullzero"`
IncludeSLOs bool `bun:"include_slos,notnull"`
Enabled bool `bun:"enabled,notnull"`
// LastPeriodStart is the UTC start of the last period actually reported.
// It is the duplicate-run suppression key: a second job fire for the same
// closed period is a no-op, which is what makes multi-replica scheduling
// safe without leader election.
LastPeriodStart *time.Time `bun:"last_period_start"`
LastRunAt *time.Time `bun:"last_run_at"`
CreatedAt time.Time `bun:"created_at,notnull"`
UpdatedAt time.Time `bun:"updated_at,notnull"`
DeletedAt *time.Time `bun:"deleted_at"`
}
ReportSchedule is a recurring uptime-report digest.
It is deliberately NOT a column on SLO: a digest is useful for checks that carry no formal objective at all, and an SLO is useful without anybody being emailed about it.
func NewReportSchedule ¶
func NewReportSchedule(orgUID, name, frequency string) *ReportSchedule
NewReportSchedule builds a report schedule with a generated UID and defaults.
func (*ReportSchedule) IsOrgWide ¶
func (r *ReportSchedule) IsOrgWide() bool
IsOrgWide reports whether the schedule covers every check in the org.
type ReportScheduleUpdate ¶
type ReportScheduleUpdate struct {
Name *string
Frequency *string
Timezone *string
Recipients *[]string
CheckUIDs *[]string
CheckGroupUIDs *[]string
IncludeSLOs *bool
Enabled *bool
}
ReportScheduleUpdate carries the fields a PATCH may change. Nil = leave alone.
type Result ¶
type Result struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
CheckUID string `bun:"check_uid,notnull"`
PeriodType string `bun:"period_type,notnull"`
PeriodStart time.Time `bun:"period_start,notnull"`
PeriodEnd *time.Time `bun:"period_end"`
Region *string `bun:"region"`
// Raw result fields (period_type = 'raw')
WorkerUID *string `bun:"worker_uid"`
Status *int `bun:"status"`
Duration *float32 `bun:"duration"`
Metrics JSONMap `bun:"metrics,type:jsonb,nullzero"`
Output JSONMap `bun:"output,type:jsonb,nullzero"`
// Maintenance records that an active maintenance window covered this
// check at the moment the probe was recorded (spec 2026-08-20-01). It is
// set at ingest and never backfilled: rollup buckets cannot be sliced
// after the fact, so the tag has to exist before aggregation runs. Raw
// rows only.
//
// It changes NO existing availability number. Status pages, badges and the
// availability API keep counting maintenance probes exactly as before;
// only an SLO with ExcludeMaintenance set subtracts them.
Maintenance bool `bun:"maintenance,notnull"`
// Diagnostics is the opt-in capture of what the probe saw (spec
// 2026-08-20-01). It is TRANSIENT by construction: `bun:"-"` keeps it out
// of every INSERT/UPDATE/SELECT so it can never reach the `output` JSONB
// column or any other column, and `json:"-"` keeps it off every API
// response built from this struct. It exists on the model only so the
// result-submission path can hand it to the incident pipeline, which is
// the ONLY thing allowed to persist it (onto incidents.details, and only
// on an incident open/reopen).
Diagnostics *checkerdef.Diagnostics `bun:"-" json:"-"`
// Aggregated fields (period_type = 'hour', 'day', 'month', 'year').
// availability_pct is intentionally absent: it is derived at read time from
// successful_checks / total_checks rather than stored (spec 2026-07-24-02).
TotalChecks *int `bun:"total_checks"`
SuccessfulChecks *int `bun:"successful_checks"`
// MaintenanceChecks / MaintenanceSuccessfulChecks are SUBSETS of the two
// counters above: how many of the probes folded into this bucket were
// recorded while an active maintenance window covered the check
// (spec 2026-08-20-01).
//
// They are additive across tiers exactly like their parents, and the tiers
// are disjoint by construction (the aggregation job compacts and deletes in
// one transaction), so a month row's counters are the exact count of raw
// probes it descends from — no double counting.
//
// Being subsets rather than replacements is what keeps this change inert
// for every existing consumer: availability stays successful/total.
MaintenanceChecks *int `bun:"maintenance_checks"`
MaintenanceSuccessfulChecks *int `bun:"maintenance_successful_checks"`
DurationMin *float32 `bun:"duration_min"`
DurationMax *float32 `bun:"duration_max"`
DurationP95 *float32 `bun:"duration_p95"`
DurationAvg *float32 `bun:"duration_avg"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
// CheckSlug and CheckName are populated only when the query joined
// `checks` (ListResultsFilter.IncludeCheckInfo, set when the results
// endpoint's caller requests with=checkSlug,checkName). Scan-only and
// transient — never selected, inserted, or updated outside that query;
// bun scans the joined `check_slug`/`check_name` aliases straight into
// these tags without a real relation. A LEFT JOIN leaves both nil for a
// result whose check has since been hard-deleted (results can outlive a
// hard-deleted check, see ListResultsFilter.RequireCheckExists) rather
// than dropping the row from the page.
CheckSlug *string `bun:"check_slug,scanonly"`
CheckName *string `bun:"check_name,scanonly"`
}
Result represents a check execution result.
func NewResult ¶
func NewResult(orgUID, checkUID string, status ResultStatus, duration float32) *Result
NewResult creates a new raw result with generated UID.
func (*Result) ExcludedFromAvailability ¶
ExcludedFromAvailability reports whether a raw result must be dropped from both availability's numerator and denominator: a lifecycle marker (still in flight — created/running) OR a row the abandoned-result reaper finalized (ResultStatusAbandoned — terminal, but evidence of OUR infrastructure failing, not the monitored service — spec 2026-08-18-03). RawAvailability, the hour rollup's processRawResult (job_aggregation.go), and uptimebar's accumulateRaw (uptimebar/bucketing.go) all route through this one predicate so the three surfaces can never drift apart on what counts.
A row with no status at all is NOT excluded: that is an aggregated rollup row, which never reaches this predicate's raw-only callers, and treating a missing status as "skip" would silently swallow a malformed raw row instead of counting it.
type ResultStatus ¶
type ResultStatus int
ResultStatus represents the status of a check result.
const ( // ResultStatusCreated indicates the check was just created and hasn't been executed yet. ResultStatusCreated ResultStatus = 1 // ResultStatusRunning indicates the check process has started but not yet completed. ResultStatusRunning ResultStatus = 2 // ResultStatusUp indicates the check passed successfully. ResultStatusUp ResultStatus = 3 // ResultStatusDown indicates the check failed. ResultStatusDown ResultStatus = 4 // ResultStatusTimeout indicates the check timed out. ResultStatusTimeout ResultStatus = 5 // ResultStatusError indicates the check encountered an error. ResultStatusError ResultStatus = 6 // ResultStatusDegraded is the aggregated rollup status: a window contained // warning(s) but no dominating failure. Stored only on aggregated rows // (period_type = hour/day/month) by the aggregation job, never on raw rows. ResultStatusDegraded ResultStatus = 7 // ResultStatusWarning indicates the target is up but there is something to // report. Stored on raw rows; counts as up for availability. ResultStatusWarning ResultStatus = 8 // ResultStatusAbandoned marks a raw attempt the abandoned-result reaper // finalized from a stale `created` marker (spec 2026-08-18-03): nothing was // ever reported for it because OUR side died — a worker crash, a devloop // restart, a lost lease — not because the monitored service was down. It is // terminal (the timeline keeps honest evidence an attempt happened) but is // excluded from availability math everywhere, unlike a genuine // ResultStatusError; see ExcludedFromAvailability. // // Server-minted ONLY, by ReapAbandonedResults. A worker or a deported agent // can never report it: handlers/agentws validates inbound statuses to // [ResultStatusCreated..ResultStatusError] and so rejects 9 outright. ResultStatusAbandoned ResultStatus = 9 )
func (ResultStatus) CountsAsUp ¶
func (s ResultStatus) CountsAsUp() bool
CountsAsUp reports whether a raw result counts toward availability success. Warning is "up with something to report" (the target is reachable), so it counts.
func (ResultStatus) ExcludedFromAvailability ¶
func (s ResultStatus) ExcludedFromAvailability() bool
ExcludedFromAvailability reports whether a raw status must be dropped from both availability's numerator and denominator: a lifecycle marker (still in flight — created/running) OR ResultStatusAbandoned (terminal, but evidence of OUR infrastructure failing, not the monitored service — spec 2026-08-18-03). This is the one place the rule lives; (*Result) delegates to it so a caller holding only a status and a caller holding a whole row can never disagree.
func (ResultStatus) IsLifecycleMarker ¶
func (s ResultStatus) IsLifecycleMarker() bool
IsLifecycleMarker reports whether the status is a non-measurement lifecycle state (created/running) that must be excluded from availability denominators.
type RotationType ¶
type RotationType string
RotationType is the cadence at which a schedule rotates between users.
const ( // RotationTypeDaily rotates every day at handoff_time. RotationTypeDaily RotationType = "daily" // RotationTypeWeekly rotates once a week, at handoff_time on handoff_weekday. RotationTypeWeekly RotationType = "weekly" )
type SLO ¶
type SLO struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
Name string `bun:"name,notnull"`
Slug string `bun:"slug,notnull"`
// Exactly one of CheckUID / CheckGroupUID is set — enforced by the
// slos_scope_xor CHECK constraint, not merely by convention.
CheckUID *string `bun:"check_uid"`
CheckGroupUID *string `bun:"check_group_uid"`
// TargetPct is the objective, e.g. 99.9. 0 < TargetPct <= 100.
TargetPct float64 `bun:"target_pct,notnull"`
// Timezone is the IANA zone the calendar month is resolved in.
Timezone string `bun:"timezone,notnull"`
// ExcludeMaintenance subtracts probes tagged results.maintenance from this
// SLO's denominator. It affects nothing outside the SLO read path.
ExcludeMaintenance bool `bun:"exclude_maintenance,notnull"`
Enabled bool `bun:"enabled,notnull"`
CreatedAt time.Time `bun:"created_at,notnull"`
UpdatedAt time.Time `bun:"updated_at,notnull"`
DeletedAt *time.Time `bun:"deleted_at"`
}
SLO represents a service-level objective over one check or one check group.
Windows are calendar months in Timezone and nothing per-window is stored: attainment, error budget and history are always recomputed at read time off the permanent `month` rollups. The emailed uptime report is the only frozen artifact.
type SLOAlertPolicy ¶
type SLOAlertPolicy struct {
// Named explicitly rather than left to the inflector: "SLOAlertPolicy" is
// exactly the acronym-prefixed shape struct-name pluralization gets wrong.
bun.BaseModel `bun:"table:slo_alert_policies,alias:slo_alert_policies"`
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
SLOUID string `bun:"slo_uid,notnull"`
// Kind is the built-in identity, unique per SLO.
Kind string `bun:"kind,notnull"`
// Enabled defaults to false: upgrading to a version that has alerting must
// never start paging on its own.
Enabled bool `bun:"enabled,notnull"`
// LongWindowSeconds / ShortWindowSeconds bound the two rolling windows the
// evaluator measures, both ending at "now".
LongWindowSeconds int `bun:"long_window_seconds,notnull"`
ShortWindowSeconds int `bun:"short_window_seconds,notnull"`
// Threshold is the burn-rate multiple both windows must exceed. 1.0 spends
// the calendar budget exactly by period end; 14.4 spends a 30-day budget in
// about two hours.
Threshold float64 `bun:"threshold,notnull"`
Severity string `bun:"severity,notnull"`
// MinSamples is the per-window probe floor. Below it the window is
// INCONCLUSIVE: it does not fire, and it equally does not count as "below
// threshold" for the resolution hysteresis.
MinSamples int `bun:"min_samples,notnull"`
// LastEvaluatedAt / LastLongBurnRate / LastShortBurnRate are the live
// readout the dashboard's Alerting section renders. Stored so the UI and
// the evaluator cannot disagree about what the burn rate was a minute ago.
LastEvaluatedAt *time.Time `bun:"last_evaluated_at"`
LastLongBurnRate *float64 `bun:"last_long_burn_rate"`
LastShortBurnRate *float64 `bun:"last_short_burn_rate"`
// BelowThresholdSince is the hysteresis anchor: the instant BOTH windows
// first dropped below Threshold. Resolution waits until that has held for a
// full short window; it is cleared the moment either window goes back over,
// so a flapping burn never resolves.
BelowThresholdSince *time.Time `bun:"below_threshold_since"`
CreatedAt time.Time `bun:"created_at,notnull"`
UpdatedAt time.Time `bun:"updated_at,notnull"`
}
SLOAlertPolicy is one built-in multiwindow burn-rate policy attached to one SLO.
Google-SRE-style multiwindow alerting: the LONG window proves the burn is significant, the SHORT window proves it is still happening. An alert fires only when both exceed Threshold, which is precisely what stops a spike that ended forty minutes ago from paging for the rest of the hour.
Thresholds and windows are columns rather than constants because the SRE-workbook numbers are a starting point, not a law: an org whose traffic shape makes 14.4x twitchy has to be able to retune it without a deploy.
func NewSLOAlertPolicy ¶
func NewSLOAlertPolicy(orgUID, sloUID string, def *SLOAlertPolicyDefault) *SLOAlertPolicy
NewSLOAlertPolicy builds a policy row from a built-in default.
func (*SLOAlertPolicy) LongWindow ¶
func (p *SLOAlertPolicy) LongWindow() time.Duration
LongWindow returns the long window as a duration.
func (*SLOAlertPolicy) ShortWindow ¶
func (p *SLOAlertPolicy) ShortWindow() time.Duration
ShortWindow returns the short (confirmation) window as a duration.
type SLOAlertPolicyDefault ¶
type SLOAlertPolicyDefault struct {
Kind string
LongWindowSeconds int
ShortWindowSeconds int
Threshold float64
Severity string
}
SLOAlertPolicyDefault describes one built-in policy's shipped configuration.
func DefaultSLOAlertPolicies ¶
func DefaultSLOAlertPolicies() []SLOAlertPolicyDefault
DefaultSLOAlertPolicies is the shipped pair, following the SRE-workbook 99.9% table: fast = 1h/5m at 14.4x (2% of a 30-day budget per hour), slow = 6h/30m at 6x.
type SLOAlertPolicyUpdate ¶
type SLOAlertPolicyUpdate struct {
Enabled *bool
LongWindowSeconds *int
ShortWindowSeconds *int
Threshold *float64
Severity *string
MinSamples *int
LastEvaluatedAt *time.Time
LastLongBurnRate *float64
LastShortBurnRate *float64
BelowThresholdSince *time.Time
// ClearBelowThresholdSince sets the hysteresis anchor back to NULL — used
// the instant either window climbs back over the threshold.
ClearBelowThresholdSince bool
// ClearLastBurnRates nulls the live readout when a window turns
// inconclusive, so the UI shows "no data" rather than a stale number.
ClearLastBurnRates bool
}
SLOAlertPolicyUpdate carries the fields a PATCH may change plus the evaluator's state writes. Nil means "leave alone".
type SLOUpdate ¶
type SLOUpdate struct {
Name *string
Slug *string
CheckUID *string
CheckGroupUID *string
TargetPct *float64
Timezone *string
ExcludeMaintenance *bool
Enabled *bool
}
SLOUpdate carries the fields a PATCH may change. Nil means "leave alone".
type SectionSelector ¶
type SectionSelector struct {
// All selects every check in the organization. Internal checks are
// excluded (see Filter) — auto-publishing an internal probe onto a public
// page is precisely the disclosure footgun this feature has to avoid.
All bool `json:"all,omitempty"`
// Labels selects checks carrying ALL of these exact key=value labels.
Labels map[string]string `json:"labels,omitempty"`
}
SectionSelector is the dynamic-membership rule of a status page section (spec 2026-08-29-11). A section with a selector has its check resources MATERIALIZED by the reconciler rather than hand-curated: real StatusPageResource rows flagged ManagedBySelector, so every downstream consumer (availability enrichment, positions, badge/summary/embed, publications' affectedResources) keeps working unchanged.
Exactly one of the two shapes is legal:
{"all": true} — every non-internal check in the org
{"labels": {"env": "prod", "public": "true"}} — AND over exact key=value pairs
Values are exact in v1: there is no existence-only ("*") matching. `all` and `labels` are mutually exclusive, and an empty `labels` object is rejected rather than quietly meaning `all` — "select everything" must be typed out.
func (*SectionSelector) Equal ¶
func (sel *SectionSelector) Equal(other *SectionSelector) bool
Equal reports whether two selectors describe the same membership rule. Used to decide whether a section update actually changed anything.
func (*SectionSelector) Filter ¶
func (sel *SectionSelector) Filter() *ListChecksFilter
Filter renders the selector as a ListChecksFilter, so selector matching is literally the same query the checks list uses — there is no second matching implementation to drift.
Internal is left nil on purpose, which ListChecks reads as `internal = FALSE`. Internal checks are the org's own plumbing probes; a selector must never sweep one onto a status page.
func (*SectionSelector) Validate ¶
func (sel *SectionSelector) Validate() error
Validate reports whether the selector is a legal v1 selector.
type Severity ¶
type Severity struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
Slug string `bun:"slug,notnull"`
Name string `bun:"name,notnull"`
Description *string `bun:"description"`
Channels string `bun:"channels,notnull"`
IsDefault bool `bun:"is_default,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
Severity is the per-org channel-set primitive for escalation steps. Spec 2026-05-08-03 introduces it so a step can say "page critically" and fan out to email + sms + voice in a single tick rather than a chain of single-channel steps.
Channels is stored as a JSON-encoded array of channel-type strings: "email", "slack", "discord", … from ConnectionType, plus the synthetic direct-channel types "sms"/"voice"/"push"/"critical_push" that gate behind future provider integrations.
func NewSeverity ¶
NewSeverity creates a Severity with a generated UID and the given channel-type list serialized to its `channels` column.
func (*Severity) ChannelList ¶
ChannelList returns the parsed channel-type list from the JSON-encoded Channels column. Returns an empty slice on parse error so callers can treat the row as "fan out to nothing" rather than crashing.
type SeveritySeed ¶
SeveritySeed describes one of the per-org default severities seeded when an organization is first created. Kept in models so both the migrate path and the org-creation hook can share the source of truth.
func DefaultSeveritySeeds ¶
func DefaultSeveritySeeds() []SeveritySeed
DefaultSeveritySeeds returns the three rows seeded for every new org. The "default" row is the org's default severity used by escalation steps that omit `severityUid` and target a user / all_admins.
type SeverityUpdate ¶
type SeverityUpdate struct {
Slug *string
Name *string
Description *string
Channels *[]string
IsDefault *bool
ClearDefault bool
}
SeverityUpdate represents fields that can be updated on a severity. Pointer-typed fields use nil to mean "leave alone"; channel slices use a separate flag because []string{} is itself a meaningful value ("page nothing").
type SignupAttribution ¶
type SignupAttribution struct {
Source string `json:"utmSource,omitempty"`
Medium string `json:"utmMedium,omitempty"`
Campaign string `json:"utmCampaign,omitempty"`
Term string `json:"utmTerm,omitempty"`
Content string `json:"utmContent,omitempty"`
// ClickIDKind names the query parameter the click id arrived in — gclid,
// gbraid, wbraid (Google) or msclkid (Microsoft) — so the upload goes to
// the right network.
ClickIDKind string `json:"clickIdKind,omitempty"`
ClickID string `json:"clickId,omitempty"`
// LandingPath is the dashboard path the tagged link opened, without its
// query string. Useful for telling an ad landing from a docs deep link.
LandingPath string `json:"landingPath,omitempty"`
// CapturedAt is when the dashboard first saw the tags, which can be days
// before the account exists (the confirmation email is not always opened
// at once).
CapturedAt *time.Time `json:"capturedAt,omitempty"`
}
SignupAttribution is the campaign context of a signup, as forwarded by the marketing site (www.solidping.io appends it to every link into the dashboard). Every field is optional and every value is an opaque string chosen by whoever wrote the ad campaign — none of it identifies the person.
The click identifier is the one field with a purpose beyond reporting: an offline conversion upload to the ad network needs the click id and the time the account was created, and nothing else. That is the whole reason this is persisted rather than only counted in analytics.
func (*SignupAttribution) IsEmpty ¶
func (a *SignupAttribution) IsEmpty() bool
IsEmpty reports whether nothing at all was captured, so callers can store a nil pointer instead of an empty object.
type SlackSettings ¶
type SlackSettings struct {
TeamID string `json:"team_id"`
TeamName string `json:"team_name"`
BotUserID string `json:"bot_user_id"`
AccessToken string `json:"access_token"`
ChannelID string `json:"channel_id,omitempty"`
ChannelName string `json:"channel_name,omitempty"`
DestinationType string `json:"destination_type,omitempty"` // "channel" | "dm" | ""
DisplayName string `json:"display_name,omitempty"` // "#alerts" or "@alice"
InstalledByUserID string `json:"installed_by_user_id"`
Scopes []string `json:"scopes"`
// MentionOnCall makes channel alerts ping the humans the escalation policy
// would page first (`<@U123ABC>`), so a channel message tells the
// responsible person it is theirs.
//
// Zero value is deliberately false: every integration stored before this
// field existed has no such key in its settings JSON, decodes to false, and
// keeps behaving exactly as before — no backfill, no migration. New Slack
// integrations get `true` written explicitly by the install flow (see
// slack.Service.createOrUpdateConnection).
MentionOnCall bool `json:"mention_on_call"`
// CommentIngestion selects how inbound Slack thread replies are treated:
//
// - SlackCommentIngestionExplicit (default, and the meaning of an empty
// value): only an explicit `/comment` becomes an incident comment.
// Triage chatter in the thread — "lunch?", "who's on call?" — stays
// chatter instead of becoming permanent incident-timeline content.
// - SlackCommentIngestionAll: every human thread reply is ingested, the
// historical behavior, kept for teams that want it.
//
// Zero value is deliberately the safe one: an integration stored before
// this field existed decodes to "" and therefore stops over-capturing,
// which is the whole point of the change.
CommentIngestion string `json:"comment_ingestion,omitempty"`
}
SlackSettings represents Slack-specific settings stored in the Settings JSONB.
func SlackSettingsFromJSONMap ¶
func SlackSettingsFromJSONMap(m JSONMap) (*SlackSettings, error)
SlackSettingsFromJSONMap parses SlackSettings from a JSONMap.
func (*SlackSettings) DMCaptureAvailable ¶
func (s *SlackSettings) DMCaptureAvailable() bool
DMCaptureAvailable reports whether this workspace's install granted the scope needed to receive direct messages.
Slack DOES NOT GRANT NEW SCOPES TO EXISTING INSTALLS: a workspace that connected before im:history was requested keeps its old grant until someone re-runs the install. That is a user-visible migration, not a deploy, so the state is surfaced (in the integration UI, in the docs, and as a gauge) rather than left to look like an empty inbox.
func (*SlackSettings) IngestsAllThreadReplies ¶
func (s *SlackSettings) IngestsAllThreadReplies() bool
IngestsAllThreadReplies reports whether this Slack integration captures every human thread reply as an incident comment. Anything other than an explicit "all" — including an absent value on a pre-existing row — means no.
func (*SlackSettings) ToJSONMap ¶
func (s *SlackSettings) ToJSONMap() (JSONMap, error)
ToJSONMap converts SlackSettings to JSONMap for storage.
type StateEntry ¶
type StateEntry struct {
UID string `bun:"uid,pk"`
OrganizationUID *string `bun:"organization_uid"`
UserUID *string `bun:"user_uid"`
Key string `bun:"key,notnull"`
Value *JSONMap `bun:"value,type:jsonb"`
ExpiresAt *time.Time `bun:"expires_at"`
CreatedAt time.Time `bun:"created_at,notnull"`
UpdatedAt time.Time `bun:"updated_at,notnull"`
DeletedAt *time.Time `bun:"deleted_at"`
}
StateEntry represents a key-value state entry. Used for storing notification state (Slack threads, Discord messages), user-scoped tokens (email confirmation, password reset), and distributed locking (preventing duplicate notifications).
func NewStateEntry ¶
func NewStateEntry(orgUID *string, key string) *StateEntry
NewStateEntry creates a new state entry with generated UID.
func NewUserStateEntry ¶
func NewUserStateEntry(userUID, key string) *StateEntry
NewUserStateEntry creates a new user-scoped state entry.
type StatusPage ¶
type StatusPage struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
Name string `bun:"name,notnull"`
Slug string `bun:"slug,notnull"`
Description *string `bun:"description"`
Visibility string `bun:"visibility,notnull"`
IsDefault bool `bun:"is_default,notnull"`
// Enabled, ShowAvailability and ShowResponseTime all default to TRUE in the
// DDL and all three are legal as `false` on create, so none of them may
// carry a `default:` clause here — see the AutoPublishDelaySeconds note
// below for why the tag would make the zero value unwritable. NewStatusPage
// supplies the true-by-default, and the DDL default still covers rows
// inserted outside the application (spec 2026-08-30-04).
Enabled bool `bun:"enabled,notnull"`
ShowAvailability bool `bun:"show_availability,notnull"`
ShowResponseTime bool `bun:"show_response_time,notnull"`
// HistoryDays is the deprecated back-compat column; HistoryPeriod is the
// source of truth and NewStatusPage sets both. Its `default:90` was dropped
// with the rest rather than kept as the one surviving exception: nothing
// writes 0 here, so the tag bought nothing, and keeping it would mean
// carrying a permanent allowlist entry in the guard test below for a
// deprecated column (spec 2026-08-30-04, "open question" — decided: drop).
HistoryDays int `bun:"history_days,notnull"`
HistoryPeriod string `bun:"history_period,notnull"`
Language *string `bun:"language"`
// AutoPublish turns the incident auto-publication pipeline on for this
// page. The DDL default is FALSE so that upgrading an existing
// installation never makes yesterday's internal blips public; NEW pages
// opt in through NewStatusPage instead (spec 2026-08-19-08).
AutoPublish bool `bun:"auto_publish,notnull"`
// AutoPublishDelaySeconds debounces publication: an incident must still be
// open this long after it opened before customers hear about it.
//
// 0 is legal and means "publish immediately", which is why the bun tag
// carries NO `default:` clause even though the column has one. bun omits a
// zero-valued field from an INSERT when the tag declares a default, so
// `default:60` here would silently turn an operator's deliberate "publish
// immediately" into a one-minute delay — the value would never reach the
// database at all. The DDL default still applies to rows inserted outside
// the application (an upgraded installation's existing pages).
AutoPublishDelaySeconds int `bun:"auto_publish_delay_seconds,notnull"`
// AutoResolve decides what an auto-created publication does when its
// incident resolves: always | if_untouched | never.
AutoResolve string `bun:"auto_resolve,notnull"`
// CustomCSS is operator-authored CSS injected into the public status page
// as a <style> text node (never dangerouslySetInnerHTML). nil = none.
// Capped at 64 KB and @import-free by API validation; unlike the
// custom-domain columns it IS exposed on public responses, since the
// public renderer is its only consumer.
CustomCSS *string `bun:"custom_css"`
// CustomDomain is a customer-owned hostname (punycode/ASCII, lowercased)
// the page is served on. nil = none. Globally unique among live rows.
CustomDomain *string `bun:"custom_domain"`
// CustomDomainToken is the opaque, DNS-label-safe token (lowercase base32),
// set while a domain is configured; in token mode it is the leading label of
// the expected CNAME target. Never exposed on public endpoints.
CustomDomainToken *string `bun:"custom_domain_token"`
// CustomDomainVerifiedAt is when the domain last passed CNAME verification.
// nil = unverified — only verified pages are served on the custom host.
CustomDomainVerifiedAt *time.Time `bun:"custom_domain_verified_at"`
// CustomDomainCheckedAt is when the periodic re-verification job last
// checked this domain.
CustomDomainCheckedAt *time.Time `bun:"custom_domain_checked_at"`
// CustomDomainFailures counts consecutive re-verification failures. At
// CustomDomainGraceAfterFailures the domain enters `grace` (still served);
// only at CustomDomainHardDemoteAfterFailures is the verification cleared
// (domain release/takeover protection).
CustomDomainFailures int `bun:"custom_domain_failures,notnull"`
// CustomDomainState is the explicit lifecycle state — one of the
// CustomDomainState* constants. It exists because a single failure counter
// was doing two jobs ("flaky right now" and "gone for good"), which made a
// DNS blip indistinguishable from a domain transfer and took status pages
// dark permanently (spec 2026-08-23-03). NewStatusPage sets it — the tag
// must not carry the DDL's `default:'none'`, or bun would drop any state
// the Go side sets to its zero value from the INSERT entirely.
CustomDomainState string `bun:"custom_domain_state,notnull"`
// CustomDomainSuccesses counts consecutive SUCCESSFUL re-verifications. It
// is the counter re-promotion is earned with: a demoted domain needs
// CustomDomainRepromoteSuccesses of them in a row (plus a still-valid
// certificate) before the sweep trusts it again.
CustomDomainSuccesses int `bun:"custom_domain_successes,notnull"`
// CustomDomainGraceSince is when the domain last entered `grace`. nil
// outside grace. Makes "how long has this been degrading" readable instead
// of inferred from a counter times a job interval.
CustomDomainGraceSince *time.Time `bun:"custom_domain_grace_since"`
// CustomDomainLastCheck is a human-readable diagnostic from the last
// re-verification: the mode used, the expected CNAME target, what DNS
// actually returned, and the lookup error if any. Without it, "verification
// fails but dig says the record is right" is only investigable by
// correlating server logs with manual dig runs.
CustomDomainLastCheck *string `bun:"custom_domain_last_check"`
// PasswordHash is the password hash gating a `visibility = password` page.
// It is produced by internal/utils/passwords, whose active policy is
// argon2id by default (bcrypt is selectable) — the same policy user
// passwords use, so there is one hashing decision in the system, not two.
// It is NEVER serialized onto any response — reads expose `hasPassword`
// only. nil on public/private pages.
PasswordHash *string `bun:"password_hash"`
// KioskTokenHash is the sha256 hex of the page's kiosk token (spec
// 2026-08-29-08) — the long-lived, revocable, read-only grant a wallboard
// screen presents as `?kiosk=<token>` so it can render a `password` or
// `private` page unattended.
//
// sha256 rather than the argon2id used for PasswordHash, and that is a
// deliberate difference rather than an inconsistency: the token is 32
// bytes of CSPRNG output with no dictionary to slow down, and a TV
// re-presents it every 15-30 s, so a memory-hard verification per request
// would buy nothing and cost a lot.
//
// Like PasswordHash it is NEVER serialized — reads expose `hasKioskToken`
// only, and the plaintext token is shown exactly once, at mint time. nil
// means the page has no kiosk token.
KioskTokenHash *string `bun:"kiosk_token_hash"`
// Settings holds per-page display customization — availability color
// thresholds and the page's brand identity (logo, favicon, white-label
// opt-in) — typed rather than a free-form map so keys stay discoverable
// (specs 2026-08-03-01, 2026-08-22-03). Column is NOT NULL DEFAULT '{}'.
//
// Read the branding keys through the Settings accessors
// (Settings.LogoFileUID(), .FaviconFileUID(), .HideBranding()) rather than
// reaching into .Branding, which is nil on a page that never set one.
Settings StatusPageSettings `bun:"settings,type:jsonb,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
StatusPage represents a public status page for an organization.
func NewStatusPage ¶
func NewStatusPage(orgUID, name, slug string) *StatusPage
NewStatusPage creates a new status page with generated UID.
type StatusPageBrandingUpdate ¶
type StatusPageBrandingUpdate struct {
LogoFileUID *string
FaviconFileUID *string
HideBranding bool
}
StatusPageBrandingUpdate is the whole-SECTION writer for a status page's branding: it replaces `settings -> branding` in full on every call, so one shape expresses set, replace and clear and no caller can accidentally leave a stale file UID behind and keep a retired blob publicly reachable.
"In full" stops at the branding section. The write is a two-level JSON merge in SQL (see UpdateStatusPageBranding in each dialect), never a read-modify-write of the whole settings column in Go — that would clobber a concurrent `availability` change, which is the single most likely regression this storage move could introduce.
func (*StatusPageBrandingUpdate) BrandingPatchJSON ¶
func (u *StatusPageBrandingUpdate) BrandingPatchJSON() (string, error)
BrandingPatchJSON returns the merge patch for the `branding` SECTION alone — what Postgres concatenates onto `settings->'branding'`.
func (*StatusPageBrandingUpdate) SettingsPatchJSON ¶
func (u *StatusPageBrandingUpdate) SettingsPatchJSON() (string, error)
SettingsPatchJSON returns the merge patch rooted at `settings` — what SQLite hands to json_patch.
type StatusPageCustomDomainUpdate ¶
type StatusPageCustomDomainUpdate struct {
Domain *string
Token *string
VerifiedAt *time.Time
CheckedAt *time.Time
Failures int
// State is the lifecycle state to write. The zero value is deliberately
// NOT a legal state: every caller states its intent, and the DB layer
// normalizes "" to CustomDomainStateNone so a forgotten field clears the
// domain rather than silently keeping a stale `active`.
State string
// Successes is the consecutive-success counter (see the model field).
Successes int
// GraceSince is when the domain entered grace; nil clears it.
GraceSince *time.Time
// LastCheck is the diagnostic string from the last re-verification; nil
// clears it.
LastCheck *string
}
StatusPageCustomDomainUpdate is the whole-lifecycle writer for a status page's custom-domain columns. Every field is written verbatim (a full overwrite of all five columns plus updated_at), so it expresses every transition — set, clear, verify-now, and the periodic re-verify job — in one shape. Domain/Token nil clears the columns to NULL; VerifiedAt/CheckedAt nil clears those timestamps.
type StatusPagePeriod ¶
type StatusPagePeriod string
StatusPagePeriod is the history window a status page renders. It mirrors the badge uptime-bar vocabulary: 24h is hourly (24 one-hour buckets), the rest are daily (N 24-hour buckets). It is the source of truth for bucketing; HistoryDays is kept populated for one release for backward-compat.
const ( StatusPagePeriod24h StatusPagePeriod = "24h" StatusPagePeriod7d StatusPagePeriod = "7d" StatusPagePeriod30d StatusPagePeriod = "30d" StatusPagePeriod90d StatusPagePeriod = "90d" )
StatusPagePeriod values.
func PeriodFromDays ¶
func PeriodFromDays(days int) StatusPagePeriod
PeriodFromDays maps a legacy history_days count to the closest period enum, used to backfill rows and to accept the deprecated historyDays input for one release. 7→7d, 30→30d, everything else (including 90) →90d.
func (StatusPagePeriod) IsHourly ¶
func (p StatusPagePeriod) IsHourly() bool
IsHourly reports whether the period buckets by hour (only 24h does).
func (StatusPagePeriod) Valid ¶
func (p StatusPagePeriod) Valid() bool
Valid reports whether the period is one of the four supported values.
type StatusPageResource ¶
type StatusPageResource struct {
UID string `bun:"uid,pk,type:varchar(36)"`
SectionUID string `bun:"section_uid,notnull"`
// CheckUID is the individual check to display. nil when the resource
// targets a group.
CheckUID *string `bun:"check_uid"`
// CheckGroupUID is the check group to display as one aggregated component.
// nil when the resource targets an individual check.
CheckGroupUID *string `bun:"check_group_uid"`
PublicName *string `bun:"public_name"`
Explanation *string `bun:"explanation"`
// AutoPublish overrides the page-level auto-publish setting for this one
// resource. nil (the default) means "inherit the page" — it is a
// three-state column on purpose, so a page can be flipped on or off
// without silently rewriting every resource's intent.
AutoPublish *bool `bun:"auto_publish"`
// ManagedBySelector marks a row the section's selector owns: the
// reconciler created it and the reconciler will remove it when the check
// stops matching (spec 2026-08-29-11). Operators cannot delete or reorder
// a managed row — the selector is the source of truth for it.
//
// The inverse — a MANUAL row — is never touched by the reconciler, which
// is what makes "manual placement wins" true rather than a race.
ManagedBySelector bool `bun:"managed_by_selector,notnull"`
Position int `bun:"position,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}
StatusPageResource represents a check OR a check group assigned to a status page section (spec 2026-08-01-03). Exactly one of CheckUID / CheckGroupUID is set — the database enforces it with an XOR check constraint, mirroring MaintenanceWindowCheck.
A group resource renders as ONE public component: rolled-up status, weighted average availability across members, and maintenance from a group- or member-targeted window. Members are never listed publicly.
func NewManagedStatusPageResource ¶
func NewManagedStatusPageResource(sectionUID, checkUID string, position int) *StatusPageResource
NewManagedStatusPageResource creates a check-targeting resource OWNED by the section's selector (spec 2026-08-29-11). Identical to NewStatusPageResource apart from the ownership flag, so a materialized row is indistinguishable from a manual one to every reader — which is the whole point of materializing instead of virtualizing.
func NewStatusPageGroupResource ¶
func NewStatusPageGroupResource(sectionUID, checkGroupUID string, position int) *StatusPageResource
NewStatusPageGroupResource creates a new group-targeting resource with generated UID.
func NewStatusPageResource ¶
func NewStatusPageResource(sectionUID, checkUID string, position int) *StatusPageResource
NewStatusPageResource creates a new check-targeting resource with generated UID.
func (*StatusPageResource) IsGroup ¶
func (r *StatusPageResource) IsGroup() bool
IsGroup reports whether the resource targets a check group rather than an individual check.
type StatusPageResourceUpdate ¶
type StatusPageResourceUpdate struct {
PublicName *string
Explanation *string
Position *int
// SetAutoPublish must be true for AutoPublish to be written at all; that
// is what lets a caller reset the override back to "inherit" (SetAutoPublish
// true, AutoPublish nil) as distinct from "leave alone".
SetAutoPublish bool
AutoPublish *bool
// SetTarget switches the resource's target kind. When true, BOTH target
// columns are written: exactly one of CheckUID / CheckGroupUID must be
// non-nil and the other column is set to NULL, so the XOR constraint always
// holds. When false the target is left untouched.
SetTarget bool
CheckUID *string
CheckGroupUID *string
}
StatusPageResourceUpdate represents fields that can be updated on a resource.
type StatusPageSection ¶
type StatusPageSection struct {
UID string `bun:"uid,pk,type:varchar(36)"`
StatusPageUID string `bun:"status_page_uid,notnull"`
Name string `bun:"name,notnull"`
Slug string `bun:"slug,notnull"`
Position int `bun:"position,notnull"`
// Selector is the section's dynamic-membership rule, or nil for a
// hand-curated section (the default, and what every pre-existing section
// stays). Never defaulted to anything non-nil: auto-inclusion has to be an
// explicit, deliberate act (spec 2026-08-29-11).
Selector *SectionSelector `bun:"selector,type:jsonb"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
StatusPageSection represents a section within a status page.
func NewStatusPageSection ¶
func NewStatusPageSection(pageUID, name, slug string, position int) *StatusPageSection
NewStatusPageSection creates a new section with generated UID.
type StatusPageSectionUpdate ¶
type StatusPageSectionUpdate struct {
Name *string
Slug *string
Position *int
// SetSelector must be true for Selector to be written at all; that is what
// lets a caller CLEAR a selector (SetSelector true, Selector nil — the
// section reverts to hand-curated) as distinct from "leave alone".
SetSelector bool
Selector *SectionSelector
}
StatusPageSectionUpdate represents fields that can be updated on a section.
type StatusPageSettings ¶
type StatusPageSettings struct {
Availability *AvailabilitySettings `json:"availability,omitempty"`
// Branding is the page's brand identity — logo, favicon, white-label
// opt-in (spec 2026-08-22-03). It lives here rather than in three columns
// of its own because it is read only while RENDERING the page: nothing
// filters, joins or uniquely constrains on it, which is the rule written
// down in wiki/conventions/database.md.
Branding *BrandingSettings `json:"branding,omitempty"`
}
StatusPageSettings is the typed decode target for status_pages.settings (Postgres jsonb / SQLite text). It is the home for per-page display customization knobs — availability thresholds and brand identity — added without a two-dialect migration each time. Keep it a typed struct, not a free-form map, so keys stay discoverable and validation lives in one place.
func (StatusPageSettings) EffectiveThresholds ¶
func (s StatusPageSettings) EffectiveThresholds() (float64, float64)
EffectiveThresholds resolves the page's effective availability thresholds, nil-safe on the Availability section (StatusPageSettings itself is always a value, never nil, since it is stored NOT NULL DEFAULT '{}').
func (StatusPageSettings) FaviconFileUID ¶
func (s StatusPageSettings) FaviconFileUID() *string
FaviconFileUID returns the page's favicon file UID, nil when unset.
func (StatusPageSettings) HideBranding ¶
func (s StatusPageSettings) HideBranding() bool
HideBranding reports the page's stored white-label opt-in (false when the section is absent). It is only half of the decision — see BrandingSettings.
func (StatusPageSettings) LogoFileUID ¶
func (s StatusPageSettings) LogoFileUID() *string
LogoFileUID returns the page's logo file UID, nil when unset. Nil-safe on the section so no caller has to check it.
func (*StatusPageSettings) Scan ¶
func (s *StatusPageSettings) Scan(value any) error
Scan implements sql.Scanner for reading the JSON blob back from either engine.
type StatusPageSubscriber ¶
type StatusPageSubscriber struct {
bun.BaseModel `bun:"table:status_page_subscriber"`
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
StatusPageUID string `bun:"status_page_uid,notnull"`
// Email is nil for a webhook/slack subscriber.
Email *string `bun:"email"`
Channel SubscriberChannel `bun:"channel,notnull"`
ConfirmedAt *time.Time `bun:"confirmed_at"`
ConfirmToken string `bun:"confirm_token,notnull"`
UnsubscribeToken string `bun:"unsubscribe_token,notnull"`
Scope SubscriberScope `bun:"scope,notnull"`
IncidentUID *string `bun:"incident_uid"`
// EndpointPrivate is the AES-GCM envelope holding {url, signingSecret} for
// an endpoint channel. NEVER serialized.
EndpointPrivate *string `bun:"endpoint_private"`
// EndpointHint is the masked remnant of the URL — the only part a response
// may echo.
EndpointHint *string `bun:"endpoint_hint"`
// EndpointKey is the sha256 of the normalized URL, used only as the
// dedup key in the live-uniqueness index so the URL itself never lands in
// an index.
EndpointKey *string `bun:"endpoint_key"`
// FailureCount counts CONSECUTIVE delivery failures; a success resets it.
FailureCount int `bun:"failure_count,notnull"`
// DisabledAt is set when the circuit breaker tripped. A disabled row is
// skipped by the fan-out but kept, so the operator can see it and re-enable
// it after fixing the endpoint.
DisabledAt *time.Time `bun:"disabled_at"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
StatusPageSubscriber is a subscription to a status page's updates. The original (and only public self-serve) kind is an email address, double opt-in: a row is inactive (ConfirmedAt nil) until the visitor clicks the confirm link. Operators can additionally register webhook and Slack deliveries, which are created already confirmed — the operator IS the verification.
Email is PII: never log it in clear, never expose it in public API responses. The endpoint URL is treated with the SAME opacity and then some: it is a credential, so it lives encrypted in EndpointPrivate and only EndpointHint ever reaches a response.
func NewEndpointSubscriber ¶
func NewEndpointSubscriber( orgUID, statusPageUID string, channel SubscriberChannel, scope SubscriberScope, ) *StatusPageSubscriber
NewEndpointSubscriber creates an operator-registered webhook/slack subscription. Unlike an email subscription it is born CONFIRMED: double opt-in exists to stop someone signing up a stranger's mailbox, and an operator registering their own webhook on their own page is not that.
func NewStatusPageSubscriber ¶
func NewStatusPageSubscriber(orgUID, statusPageUID, email string, scope SubscriberScope) *StatusPageSubscriber
NewStatusPageSubscriber creates an unconfirmed subscriber with a generated UID and CreatedAt. Tokens are set by the service.
func (*StatusPageSubscriber) EmailAddress ¶
func (s *StatusPageSubscriber) EmailAddress() string
EmailAddress returns the subscriber's email, or "" for an endpoint channel.
type StatusPageUpdate ¶
type StatusPageUpdate struct {
Name *string
Slug *string
Description *string
Visibility *string
IsDefault *bool
Enabled *bool
ShowAvailability *bool
ShowResponseTime *bool
HistoryDays *int
HistoryPeriod *string
Language *string
// AutoPublish / AutoPublishDelaySeconds / AutoResolve are the incident
// auto-publication settings (spec 2026-08-19-08). nil leaves untouched.
AutoPublish *bool
AutoPublishDelaySeconds *int
AutoResolve *string
// CustomCSS updates the page's custom stylesheet. A pointer to the empty
// string clears the column (the appearance editor's "empty textarea"), a
// nil pointer leaves it untouched.
CustomCSS *string
// HideBranding flips the page-level white-label opt-in, stored in
// `settings -> branding -> hideBranding`. nil leaves it. The DB layer
// merges it into the JSON rather than overwriting the column — and folds
// it into Settings when that is set in the same call, since two
// `SET settings = ...` clauses in one UPDATE is a Postgres error.
HideBranding *bool
// PasswordHash writes the password hash gating a password page. A pointer to
// the empty string CLEARS the column (the page stopped being password
// protected); a nil pointer leaves it untouched, which is what keeps an
// unrelated PATCH from silently unlocking a page.
PasswordHash *string
// KioskTokenHash writes the page's kiosk token hash. Same three-state
// convention as PasswordHash: nil leaves it untouched, a pointer to the
// empty string CLEARS it (revoke), and a value replaces it — which is what
// makes "regenerate" invalidate the previous token with no extra bookkeeping.
KioskTokenHash *string
// Settings overwrites the whole settings column when non-nil (the caller
// — statuspages.Service — has already applied the no-deep-merge
// section-replace-or-reset semantics against the current value). A nil
// pointer leaves the column untouched.
Settings *StatusPageSettings
}
StatusPageUpdate represents fields that can be updated on a status page.
type StatusUpdate ¶
type StatusUpdate struct {
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull"`
StatusPageUID string `bun:"status_page_uid,notnull"`
SectionUID *string `bun:"section_uid"` // optional: scope to section
CheckUID *string `bun:"check_uid"` // optional: scope to check
IncidentUID *string `bun:"incident_uid"` // optional: thread under incident
// IncidentPublicationUID threads the update under a publication. A
// hand-authored publication has no incident to thread on, so this is the
// only link it gets (spec 2026-08-19-08).
IncidentPublicationUID *string `bun:"incident_publication_uid"`
Title string `bun:"title,notnull"`
BodyMarkdown string `bun:"body_markdown,notnull"`
LinkURL *string `bun:"link_url"`
Kind StatusUpdateKind `bun:"kind,notnull"`
PublishedAt time.Time `bun:"published_at,notnull,default:current_timestamp"`
// AuthorUID is the user who posted the update. nil = generated by the
// auto-publish pipeline: a machine post has no author, and attributing it
// to whichever human happens to own the org would be a lie the UI renders.
AuthorUID *string `bun:"author_uid"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
StatusUpdate is an operator-written narrative post anchored to a status page.
func NewStatusUpdate ¶
func NewStatusUpdate(orgUID, statusPageUID, authorUID string) *StatusUpdate
NewStatusUpdate creates a new StatusUpdate with generated UID and timestamps.
type StatusUpdateKind ¶
type StatusUpdateKind string
StatusUpdateKind represents the kind of a status update.
const ( // StatusUpdateKindInvestigating indicates the team is investigating. StatusUpdateKindInvestigating StatusUpdateKind = "investigating" // StatusUpdateKindIdentified indicates the issue has been identified. StatusUpdateKindIdentified StatusUpdateKind = "identified" // StatusUpdateKindMonitoring indicates the fix is being monitored. StatusUpdateKindMonitoring StatusUpdateKind = "monitoring" // StatusUpdateKindResolved indicates the issue is resolved. StatusUpdateKindResolved StatusUpdateKind = "resolved" // StatusUpdateKindMaintenance indicates a scheduled maintenance. StatusUpdateKindMaintenance StatusUpdateKind = "maintenance" // StatusUpdateKindInfo is a general informational update. StatusUpdateKindInfo StatusUpdateKind = "info" )
func (StatusUpdateKind) IsValid ¶
func (k StatusUpdateKind) IsValid() bool
IsValid returns true if the kind is one of the recognized values.
type StatusUpdatesFilter ¶
type StatusUpdatesFilter struct {
StatusPageUID string
SectionUID *string
CheckUID *string
IncidentUID *string
// IncidentPublicationUID filters to one publication's thread.
IncidentPublicationUID *string
Limit int
Offset int
}
StatusUpdatesFilter configures what to return from ListStatusUpdates.
type SubscriberChannel ¶
type SubscriberChannel string
SubscriberChannel is where a subscription delivers.
`email` is the public self-serve channel and the only one a visitor can create; `webhook` and `slack` are operator-created from the dashboard, because a random visitor pasting an incoming-webhook URL has no verification story — see the spec's note on operator-side management.
const ( SubscriberChannelEmail SubscriberChannel = "email" SubscriberChannelWebhook SubscriberChannel = "webhook" SubscriberChannelSlack SubscriberChannel = "slack" )
Subscriber channels.
func (SubscriberChannel) IsEndpoint ¶
func (c SubscriberChannel) IsEndpoint() bool
IsEndpoint reports whether the channel delivers to a URL rather than a mailbox — i.e. whether the row carries an encrypted endpoint.
func (SubscriberChannel) IsValid ¶
func (c SubscriberChannel) IsValid() bool
IsValid reports whether the channel is one of the recognized values.
type SubscriberScope ¶
type SubscriberScope string
SubscriberScope determines how broadly a subscriber is notified.
const ( // SubscriberScopePage notifies on every published update for the page. SubscriberScopePage SubscriberScope = "page" // SubscriberScopeIncident notifies only for updates threaded under a // specific incident. SubscriberScopeIncident SubscriberScope = "incident" )
func (SubscriberScope) IsValid ¶
func (s SubscriberScope) IsValid() bool
IsValid reports whether the scope is one of the recognized values.
type SupportMessage ¶
type SupportMessage struct {
bun.BaseModel `bun:"table:support_messages,alias:support_messages"`
UID string `bun:"uid,pk,type:varchar(36)"`
ThreadUID string `bun:"thread_uid,notnull,type:varchar(36)"`
Channel string `bun:"channel,notnull"`
Direction string `bun:"direction,notnull"`
Body string `bun:"body,notnull"`
Truncated bool `bun:"truncated,notnull"`
RawType string `bun:"raw_type,notnull"`
ExternalID *string `bun:"external_id"`
AuthorUID *string `bun:"author_uid"`
Delivery JSONMap `bun:"delivery,type:jsonb,nullzero"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}
SupportMessage is one message inside a thread.
func NewSupportMessage ¶
func NewSupportMessage(threadUID, channel, direction, body string, createdAt time.Time) *SupportMessage
NewSupportMessage builds a message row, applying the body cap.
type SupportReplyWindow ¶
type SupportReplyWindow struct {
// Expires reports whether this channel has a window at all. Only WhatsApp
// does.
Expires bool `json:"expires"`
// Open reports whether a free-form reply can be sent right now.
Open bool `json:"open"`
// ExpiresAt is when the window closes. Nil for non-expiring channels.
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
// Reason explains a closed window in operator-facing terms. Empty when open.
Reason string `json:"reason,omitempty"`
// CostsMoney flags channels where every reply is billed per segment.
CostsMoney bool `json:"costsMoney"`
}
SupportReplyWindow is the DERIVED answer to "can we still send a free-form reply right now?". It is computed from the last inbound message's timestamp and the channel's rule at read time and never stored, so it cannot go stale.
It is a different axis from Status. A thread can be open (the customer's question is unanswered) and yet expired (WhatsApp will no longer accept a free-form reply) — which is precisely the state an operator most needs to see, because it is the one where the product cannot do what the UI otherwise implies.
type SupportThread ¶
type SupportThread struct {
bun.BaseModel `bun:"table:support_threads,alias:support_threads"`
UID string `bun:"uid,pk,type:varchar(36)"`
Channel string `bun:"channel,notnull"`
ChannelIdentity string `bun:"channel_identity,notnull"`
ChannelContext JSONMap `bun:"channel_context,type:jsonb,nullzero"`
Subject string `bun:"subject,notnull"`
Status string `bun:"status,notnull"`
OrganizationUID *string `bun:"organization_uid"`
UserUID *string `bun:"user_uid"`
LastMessageAt time.Time `bun:"last_message_at,notnull"`
LastInboundAt *time.Time `bun:"last_inbound_at"`
UnreadCount int `bun:"unread_count,notnull"`
LastMirrorAt *time.Time `bun:"last_mirror_at"`
PendingMirrors int `bun:"pending_mirrors,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
SupportThread is one conversation with one person on one channel.
Threads belong to the INSTANCE, not to an organization. The sender of an inbound WhatsApp message is a phone number; frequently there is no org to attribute it to at all, and a message from a stranger must not be dropped for lack of one. OrganizationUID/UserUID are therefore nullable ATTRIBUTION — a hint for the operator, never an access-control boundary.
func NewSupportThread ¶
func NewSupportThread(channel, identity string, createdAt time.Time) *SupportThread
NewSupportThread builds a live thread for a channel identity.
func (*SupportThread) ReplyWindow ¶
func (t *SupportThread) ReplyWindow(now time.Time) SupportReplyWindow
ReplyWindow derives the current reply window for the thread.
type TLSStorageEntry ¶
type TLSStorageEntry struct {
bun.BaseModel `bun:"table:tls_storage"`
Key string `bun:"key,pk"`
Value []byte `bun:"value,notnull"`
ModifiedAt time.Time `bun:"modified_at,notnull"`
}
TLSStorageEntry is one asset in the certmagic key-value store backing in-server ACME (spec 2026-07-26-01). Keys are certmagic's own path-like namespace (slash-separated, no leading/trailing slash), values are opaque bytes.
SECURITY: values include ACME account keys and certificate PRIVATE KEYS. Nothing outside internal/tlsedge may read this table, and it must never be exposed through an API, export, or debug surface.
type TLSStorageKeyInfo ¶
TLSStorageKeyInfo is the metadata certmagic's Stat/List need about a stored key. It deliberately carries no value bytes so listings never pull private keys into memory.
type TLSStorageLock ¶
type TLSStorageLock struct {
bun.BaseModel `bun:"table:tls_storage_locks"`
Key string `bun:"key,pk"`
Owner string `bun:"owner,notnull"`
ExpiresAt time.Time `bun:"expires_at,notnull"`
}
TLSStorageLock is an expiring, cluster-wide lock row backing certmagic's Locker interface. The holder refreshes ExpiresAt while it works; a stale row may be taken over by any node, which is what keeps a crashed issuance from wedging renewals forever.
type TokenType ¶
type TokenType string
TokenType represents the type of user token.
const ( // TokenTypePAT represents a Personal Access Token. TokenTypePAT TokenType = "pat" // TokenTypeRefresh represents a refresh token for session management. TokenTypeRefresh TokenType = "refresh" // TokenTypeOAuthRefresh represents a rotating OAuth 2.1 refresh grant for // the MCP resource (spec 2026-06-20-03). The grant's client_id, scope, and // resource bindings ride in Properties; revocation is the row's soft // delete. Each redemption endpoint validates its own type, so the three // types can never be exchanged for one another. TokenTypeOAuthRefresh TokenType = "oauth_refresh" )
type TwilioSettings ¶
type TwilioSettings struct {
AccountSID string `json:"account_sid"`
AuthToken string `json:"auth_token,omitempty"`
FromNumber string `json:"from_number,omitempty"`
MessagingServiceSID string `json:"messaging_service_sid,omitempty"`
VoiceFromNumber string `json:"voice_from_number,omitempty"`
ToNumbers []string `json:"to_numbers,omitempty"`
// Region is the Twilio regional edition this account was provisioned in
// ("" or "us1" = the default global/US1 edge, "ie1" = Ireland, "au1" =
// Australia, ...). Empty behaves exactly as before this field existed.
Region string `json:"region,omitempty"`
}
TwilioSettings is the public (queryable) side of a Twilio connection's Settings JSONB. The matching secret — the account auth token — lives encrypted in SettingsPrivate under the "auth_token" key (see connectionSecretFields). Exactly one of FromNumber / MessagingServiceSID is set for SMS; VoiceFromNumber (optional) enables voice calls; ToNumbers are shared recipients for direct-channel (registry-path) sends. Region is public/non-secret: it picks which Twilio API host requests go to (see twilio.BaseURLForRegion) but carries no credential material itself.
func TwilioSettingsFromJSONMap ¶
func TwilioSettingsFromJSONMap(m JSONMap) (*TwilioSettings, error)
TwilioSettingsFromJSONMap parses TwilioSettings from a JSONMap. The map is expected to be the decrypt-and-merged Settings (auth_token present).
func (*TwilioSettings) ToJSONMap ¶
func (ts *TwilioSettings) ToJSONMap() (JSONMap, error)
ToJSONMap converts TwilioSettings to JSONMap for storage.
type User ¶
type User struct {
UID string `bun:"uid,pk,type:varchar(36)"`
Email string `bun:"email,notnull"`
Name string `bun:"name"`
AvatarURL string `bun:"avatar_url"`
PasswordHash *string `bun:"password_hash"`
EmailVerifiedAt *time.Time `bun:"email_verified_at"`
SuperAdmin bool `bun:"super_admin"`
TOTPSecret *string `bun:"totp_secret"`
TOTPEnabled bool `bun:"totp_enabled,notnull"`
TOTPRecoveryCodes []string `bun:"totp_recovery_codes,type:jsonb"`
// MustChangePassword forces a password rotation before the account can do
// anything else. It is a GENERAL user-level capability, not a property of
// the seeded bootstrap admin: an operator-initiated reset, an invited user
// or a compromised-credential response all set the same flag, and every
// consumer reads this field rather than keying on who the user is.
//
// While it is true, a session authenticated as this user reaches only the
// rotation endpoint, /auth/me and /auth/logout — enforced centrally in the
// auth layer (see internal/handlers/auth/password_rotation.go), so the API,
// the dashboard, the CLI, PAT creation and the realtime socket are all
// covered by one rule.
//
// Defaults to false, which is what keeps OAuth/SSO/LDAP users — who may
// carry a nil PasswordHash and could not satisfy a rotation — unaffected.
MustChangePassword bool `bun:"must_change_password,notnull"`
// Demo marks the shared public-demo principal (spec 2026-09-06-02). Like
// MustChangePassword it is a GENERAL user-level capability, not a property
// of one seeded email: the JWT/PAT claims minted for such a user carry it,
// the write guard in RequireAuth keys off it, and the demo cleanup job
// reconciles whatever it names. Nothing anywhere matches on the address.
//
// A demo session may write exactly four things (see
// handlers/auth/demo_guard.go); everything else answers 403 DEMO_READ_ONLY.
Demo bool `bun:"demo,notnull"`
// SignupAttribution records where the signup came from — the campaign tags
// and ad click identifier the marketing site forwarded on the link that
// led here (spec 2026-09-07-03). Set once, at account creation, and never
// updated: it is a historical fact about the signup, not a live property
// of the user. Nil for every account that did not arrive from a tagged
// link, which is most of them. Stored as JSON so the shape can grow
// (another ad network's click id) without a migration.
SignupAttribution *SignupAttribution `bun:"signup_attribution,type:jsonb"`
LastActiveAt *time.Time `bun:"last_active_at"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
User represents a global user account.
type UserContact ¶
type UserContact struct {
bun.BaseModel `bun:"table:user_contacts"`
UID string `bun:"uid,pk,type:varchar(36)"`
UserUID string `bun:"user_uid,notnull,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull,type:varchar(36)"`
Type string `bun:"type,notnull"`
Value string `bun:"value,notnull"`
Label string `bun:"label,notnull"`
VerifiedAt *time.Time `bun:"verified_at"`
// Verification state for contact types that require a code round-trip
// (phone). VerifyCodeHash is the SHA-256 hex of the in-flight 6-digit
// code; nil when no verification is pending or after a successful confirm.
VerifyCodeHash *string `bun:"verify_code_hash"`
VerifyExpiresAt *time.Time `bun:"verify_expires_at"`
VerifyAttempts int `bun:"verify_attempts,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at,soft_delete"`
}
UserContact is one addressable endpoint for a user (email, phone, Slack DM, …). A single contact can belong to only one org — contacts are org-scoped so the correct bot token can be resolved at dispatch time.
func NewUserContact ¶
func NewUserContact(userUID, orgUID, contactType, value, label string) *UserContact
NewUserContact builds a contact with a fresh UID.
type UserIntegrationIdentity ¶
type UserIntegrationIdentity struct {
bun.BaseModel `bun:"table:user_integration_identities,alias:uii"`
UID string `bun:"uid,pk,type:varchar(36)"`
OrganizationUID string `bun:"organization_uid,notnull,type:varchar(36)"`
IntegrationUID string `bun:"integration_uid,notnull,type:varchar(36)"`
UserUID string `bun:"user_uid,notnull,type:varchar(36)"`
// ExternalID is the provider-side identifier used to address the person in
// a message (a Slack user id, rendered as `<@U123ABC>`).
ExternalID string `bun:"external_id,notnull"`
// DisplayName is the provider-side display name captured at match time.
// Purely cosmetic — used for the admin UI and for the plain-text fallback.
DisplayName string `bun:"display_name,notnull"`
// Source is IdentitySourceAuto or IdentitySourceManual.
Source string `bun:"source,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
}
UserIntegrationIdentity answers "who is this org member on this integration instance" — e.g. `U123ABC` on a specific Slack workspace. It is deliberately NOT a user_contact: contacts are "how to page me" (and carry verification state), identities are "who I am there" and are used for mentions and attribution only. Paging never reads this table.
Scoped per integration (not per integration *type*) because an org can connect several Slack workspaces, and the same person has a different user id in each.
func NewUserIntegrationIdentity ¶
func NewUserIntegrationIdentity( orgUID, integrationUID, userUID, externalID, displayName, source string, ) *UserIntegrationIdentity
NewUserIntegrationIdentity builds an identity row with a fresh UID.
type UserNotificationRoute ¶
type UserNotificationRoute struct {
bun.BaseModel `bun:"table:user_notification_routes"`
UID string `bun:"uid,pk,type:varchar(36)"`
UserUID string `bun:"user_uid,notnull,type:varchar(36)"`
OrgUID string `bun:"org_uid,notnull,type:varchar(36)"`
ContactUID string `bun:"contact_uid,notnull,type:varchar(36)"`
// No `default:true` on the tag even though the column has one — see the
// StatusPage.AutoPublishDelaySeconds note: it made a route impossible to
// CREATE disabled. NewUserNotificationRoute supplies the default.
Enabled bool `bun:"enabled,notnull"`
Position int `bun:"position,notnull"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
Contact *UserContact `bun:"rel:belongs-to,join:contact_uid=uid"`
}
UserNotificationRoute joins a UserContact to an ordered, toggle-able delivery slot. One route per contact (enforced by UNIQUE on contact_uid).
func NewUserNotificationRoute ¶
func NewUserNotificationRoute(userUID, orgUID, contactUID string, position int) *UserNotificationRoute
NewUserNotificationRoute builds a route with a fresh UID.
type UserPasskey ¶
type UserPasskey struct {
UID string `bun:"uid,pk,type:varchar(36)"`
UserUID string `bun:"user_uid,notnull"`
Name string `bun:"name,notnull"`
CredentialID []byte `bun:"credential_id,notnull"`
PublicKey []byte `bun:"public_key,notnull"`
AAGUID *string `bun:"aaguid"`
SignCount uint32 `bun:"sign_count,notnull"`
Transports []string `bun:"transports,type:jsonb,nullzero"`
BackupEligible bool `bun:"backup_eligible,notnull"`
BackupState bool `bun:"backup_state,notnull"`
UserVerified bool `bun:"user_verified,notnull"`
AttestationFormat *string `bun:"attestation_format"`
LastUsedAt *time.Time `bun:"last_used_at"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
User *User `bun:"rel:belongs-to,join:user_uid=uid"`
}
UserPasskey is a registered WebAuthn credential. The public key is not a secret, so no encryption-at-rest envelope is needed. SignCount is a monotonically-increasing replay guard reported by the authenticator; regressions indicate a cloned credential and should reject the assertion.
func NewUserPasskey ¶
func NewUserPasskey(userUID, name string, credentialID, publicKey []byte) *UserPasskey
NewUserPasskey builds a new passkey row with a generated UID.
type UserPasskeyUpdate ¶
type UserPasskeyUpdate struct {
Name *string
SignCount *uint32
LastUsedAt *time.Time
BackupState *bool
}
UserPasskeyUpdate carries the mutable subset of UserPasskey. SignCount and LastUsedAt update on every successful assertion; Name updates via the rename endpoint.
type UserProvider ¶
type UserProvider struct {
UID string `bun:"uid,pk,type:varchar(36)"`
UserUID string `bun:"user_uid,notnull"`
ProviderType ProviderType `bun:"provider_type,notnull"`
ProviderID string `bun:"provider_id,notnull"`
Metadata JSONMap `bun:"metadata,type:jsonb,nullzero"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
// Relations (for eager loading)
User *User `bun:"rel:belongs-to,join:user_uid=uid"`
}
UserProvider links a user to an external auth provider.
func NewUserProvider ¶
func NewUserProvider(userUID string, providerType ProviderType, providerID string) *UserProvider
NewUserProvider creates a new user provider with generated UID.
type UserToken ¶
type UserToken struct {
UID string `bun:"uid,pk,type:varchar(36)"`
UserUID string `bun:"user_uid,notnull"`
OrganizationUID *string `bun:"organization_uid"`
Token string `bun:"token,notnull"`
Type TokenType `bun:"type,notnull"`
Properties JSONMap `bun:"properties,type:jsonb,nullzero"`
ExpiresAt *time.Time `bun:"expires_at"`
LastActiveAt *time.Time `bun:"last_active_at"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
// Relations (for eager loading)
User *User `bun:"rel:belongs-to,join:user_uid=uid"`
Organization *Organization `bun:"rel:belongs-to,join:organization_uid=uid"`
}
UserToken represents an authentication token (PAT, session refresh token, or OAuth refresh grant).
type UserTokenUpdate ¶
UserTokenUpdate represents fields that can be updated.
type UserUpdate ¶
type UserUpdate struct {
Email *string
Name *string
AvatarURL *string
PasswordHash *string
EmailVerifiedAt *time.Time
SuperAdmin *bool
TOTPSecret *string
TOTPEnabled *bool
TOTPRecoveryCodes *[]string
// MustChangePassword sets or clears the forced-rotation flag. Nil leaves it
// alone — so an unrelated profile update can never silently un-force a
// pending rotation.
MustChangePassword *bool
// Demo sets or clears the shared-demo-principal flag. Nil leaves it alone.
Demo *bool
LastActiveAt *time.Time
}
UserUpdate represents fields that can be updated.
type Worker ¶
type Worker struct {
UID string `bun:"uid,pk,type:varchar(36)"`
Slug string `bun:"slug,notnull"`
Name string `bun:"name,notnull"`
Region *string `bun:"region"`
LastActiveAt *time.Time `bun:"last_active_at"`
// Capabilities is the worker's self-reported capability set — the names of
// the things it CAN do — refreshed alongside last_active_at (specs
// 2026-08-15-11, 2026-08-16-02). One generic set rather than a column per
// capability, so the next capability is a pure string addition.
//
// THREE STATES, NOT TWO, AND NIL IS THE ONLY UNKNOWN:
//
// nil unknown — nothing was ever reported (a worker that
// predates the feature, or has not checked in yet)
// []string{} reported, and this worker has none of them
// []string{"ipv6"} reported this exact set
//
// A NON-NIL SET IS AUTHORITATIVE AND CLOSED: absence from it means "no",
// never "unknown". Conflating the two is precisely the lie this feature
// exists to stop telling — it would paint every worker predating the
// capability report as IPv6-incapable. Read it through Capability() rather
// than by hand so the unknown case cannot be forgotten.
//
// The bun tag mirrors Check.Regions: pgdialect encodes it as the `text[]`
// the Postgres schema declares, sqlitedialect ignores `array` and stores
// the JSON array the SQLite schema expects. `nullzero` is what maps a nil
// slice to SQL NULL; a non-nil empty slice is NOT zero for bun (its zero
// checker for slices is "is nil"), so it still writes `{}` / `[]`.
Capabilities []string `bun:"capabilities,type:text[],array,nullzero"`
// Version is the worker's self-reported build version (specs
// 2026-08-19-07), refreshed alongside Capabilities. UNLIKE Capabilities,
// this is a TWO-state field, not three: a real build version is never
// the empty string, so there is no meaningful "reported, and has none"
// answer for a scalar the way there is for a set.
//
// nil unknown — nothing was ever reported (a worker that predates
// the feature, or has not sent a claim frame yet)
// &"x.y.z" the version this worker last reported
//
// nil is the only unknown, and it must never be rendered as "drifted" —
// an old agent that predates version reporting must not look broken. The
// match/drifted/unknown comparison against the server's own version is
// computed at read time (handlers/agents), not stored here.
Version *string `bun:"version"`
CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"`
DeletedAt *time.Time `bun:"deleted_at"`
}
Worker represents a distributed worker that executes checks.
func (*Worker) Capability ¶
func (w *Worker) Capability(name string) CapabilityState
Capability answers, as a tri-state, whether this worker reports capability name. A nil set is unknown; a non-nil set is closed.
Source Files
¶
- agent.go
- app_setting.go
- auth.go
- check.go
- check_connection.go
- check_dependency.go
- check_group.go
- check_group_status.go
- check_job.go
- check_regions.go
- common.go
- delivery_details.go
- device_auth.go
- discovered_check.go
- email_suppression.go
- entitlements_payload.go
- escalation_policy.go
- event.go
- file.go
- incident.go
- incident_clock_update.go
- incident_notification.go
- incident_publication.go
- integration.go
- job.go
- maintenance_window.go
- membership_request.go
- oauth.go
- on_call_schedule.go
- org_entitlements.go
- org_usage_counter.go
- organization.go
- page_status.go
- parameter.go
- recent_results.go
- report_schedule.go
- result.go
- severity.go
- slo.go
- slo_alert_policy.go
- state.go
- status_page.go
- status_page_settings.go
- status_page_subscriber.go
- status_update.go
- status_wire.go
- support.go
- tls_storage.go
- user_contact.go
- user_integration_identity.go
- worker.go