From 41c3dd292d9de4a1ebf1e016dea8e3c98e8d0799 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Wed, 8 Jul 2026 17:55:46 +0200 Subject: [PATCH 1/4] feat(alerts): first-run empty-state launcher + migrate alerts lists to Electric collections Redesign the alerts overview zero-rules empty state into a "starter launcher": a purpose-built "quiet monitor" SVG (a steady signal held below a dashed amber threshold, reusing .infra-ref-line / .infra-pulse) replaces the generic bell-badge, and the five real starter templates surface as chart-*-tinted tiles that deep-link into a pre-filled create form. Adds a `template` search param to /alerts/create; deriveInitialRuleDraft pre-applies the matching preset and skips the first-touch overlay (unknown ids fall through to blank + overlay), reusing applyTemplate so the overlay, empty-state tiles, and create_alert_rule MCP tool resolve the same set. Also bundles the concurrent alerts-data migration to TanStack DB + Electric collections: useAlertDestinationsList and the alertDestinations/scrapeTargets collections, the electric-sync shape route updates, the settings + scrape-target sections wiring, and the 0011_electric_publication_wave1 drizzle migration. Co-Authored-By: Claude Opus 4.8 --- apps/electric-sync/src/routes/shape.http.ts | 40 +- apps/electric-sync/src/routes/shape.test.ts | 30 + .../alerts/alert-create-page-content.test.ts | 48 + .../alerts/alert-create-page-content.tsx | 26 +- .../alerts/overview/alerts-empty-state.tsx | 132 + .../alerts/overview/alerts-overview-tab.tsx | 33 +- .../alerts/overview/settings-tab.tsx | 5 +- .../settings/escalation-policy-section.tsx | 5 +- .../settings/scrape-targets-section.tsx | 17 +- apps/web/src/hooks/use-alerts-list.ts | 36 +- .../web/src/hooks/use-scrape-target-checks.ts | 52 + apps/web/src/lib/collections/alerts.test.ts | 43 + apps/web/src/lib/collections/alerts.ts | 77 + .../lib/collections/org-collections.test.ts | 2 + .../src/lib/collections/org-collections.ts | 9 + .../lib/collections/scrape-targets.test.ts | 53 + .../web/src/lib/collections/scrape-targets.ts | 58 + apps/web/src/routes/alerts/$ruleId.tsx | 10 +- apps/web/src/routes/alerts/create.tsx | 2 + apps/web/src/routes/alerts/index.tsx | 5 +- .../0011_electric_publication_wave1.sql | 33 + packages/db/drizzle/meta/0011_snapshot.json | 5800 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 171 +- packages/db/src/migrations.test.ts | 3 + 24 files changed, 6546 insertions(+), 144 deletions(-) create mode 100644 apps/web/src/components/alerts/alert-create-page-content.test.ts create mode 100644 apps/web/src/components/alerts/overview/alerts-empty-state.tsx create mode 100644 apps/web/src/hooks/use-scrape-target-checks.ts create mode 100644 apps/web/src/lib/collections/scrape-targets.test.ts create mode 100644 apps/web/src/lib/collections/scrape-targets.ts create mode 100644 packages/db/drizzle/0011_electric_publication_wave1.sql create mode 100644 packages/db/drizzle/meta/0011_snapshot.json diff --git a/apps/electric-sync/src/routes/shape.http.ts b/apps/electric-sync/src/routes/shape.http.ts index 990748809..23eab16d0 100644 --- a/apps/electric-sync/src/routes/shape.http.ts +++ b/apps/electric-sync/src/routes/shape.http.ts @@ -23,8 +23,12 @@ import { SyncConfig } from "../config" */ // Server-pinned shape whitelist. Every shape is additionally org-scoped below; -// `extraWhere` narrows the synced rows further (immutable — changing it is a new -// shape name + full re-sync, so version the name if it must ever change). +// `extraWhere` narrows the synced rows further and `columns` restricts which +// columns Electric streams to the browser (drop encrypted secrets / large jsonb +// blobs — the client never needs them, and they must not leave the server). Both +// are immutable — changing either is a new shape name + full re-sync, so version +// the name if it must ever change. When `columns` is set it MUST include the +// table's primary-key column(s) (Electric requires the PK in the projection). const SHAPES = { dashboards: { table: "dashboards" }, alert_rules: { table: "alert_rules" }, @@ -33,7 +37,33 @@ const SHAPES = { error_issues: { table: "error_issues", extraWhere: `"archived_at" IS NULL` }, actors: { table: "actors" }, open_error_incidents: { table: "error_incidents", extraWhere: `"status" = 'open'` }, -} as const satisfies Record + // `config_json` holds only public config (summary / channel label / hazel + // metadata); the encrypted webhook secrets live in separate `secret_*` columns + // that MUST NOT reach the browser, so the projection drops them (and the + // unused `created_by`/`updated_by`). The PK `id` is required in the projection. + alert_destinations: { + table: "alert_destinations", + columns: [ + "id", + "org_id", + "name", + "type", + "enabled", + "config_json", + "last_tested_at", + "last_test_error", + "created_at", + "updated_at", + ], + }, + // Uptime-probe history — already pruned server-side to 24h + a 10k-per-target + // cap (see ScrapeTargetsService.pruneChecks), so the whole per-org shape stays + // bounded. No secrets in this table. + scrape_target_checks: { table: "scrape_target_checks" }, +} as const satisfies Record< + string, + { readonly table: string; readonly extraWhere?: string; readonly columns?: ReadonlyArray } +> export type ShapeName = keyof typeof SHAPES @@ -136,6 +166,10 @@ export const buildUpstreamShapeUrl = (args: { ) url.searchParams.set("params[1]", args.orgId) + // Column projection is pinned server-side too — a shape that drops secret / + // oversized columns must never let the client widen it back to `SELECT *`. + if ("columns" in def && def.columns) url.searchParams.set("columns", def.columns.join(",")) + // Electric Cloud source credentials (absent when self-hosting Electric). if (args.sourceId) url.searchParams.set("source_id", args.sourceId) if (args.secret) url.searchParams.set("secret", args.secret) diff --git a/apps/electric-sync/src/routes/shape.test.ts b/apps/electric-sync/src/routes/shape.test.ts index 9dfe00a65..f11f175fe 100644 --- a/apps/electric-sync/src/routes/shape.test.ts +++ b/apps/electric-sync/src/routes/shape.test.ts @@ -97,6 +97,36 @@ describe("buildUpstreamShapeUrl", () => { assert.isNull(params.get("replica")) }) + it("pins a shape's column projection and drops the secret columns", () => { + const { params } = parse(buildUpstreamShapeUrl({ ...base, shape: "alert_destinations" })) + const columns = params.get("columns")?.split(",") ?? [] + // PK + org scope must be present for Electric. + assert.include(columns, "id") + assert.include(columns, "org_id") + // The public config the browser renders is allowed… + assert.include(columns, "config_json") + // …but the encrypted secret columns must never be projected. + assert.notInclude(columns, "secret_ciphertext") + assert.notInclude(columns, "secret_iv") + assert.notInclude(columns, "secret_tag") + }) + + it("omits the columns param for shapes that sync every column", () => { + const { params } = parse(buildUpstreamShapeUrl({ ...base, shape: "scrape_target_checks" })) + assert.isNull(params.get("columns")) + }) + + it("never lets the client widen a column-restricted shape back to secrets", () => { + const malicious = new URLSearchParams() + malicious.set("columns", "id,org_id,secret_ciphertext,secret_iv,secret_tag") + const { params } = parse( + buildUpstreamShapeUrl({ ...base, shape: "alert_destinations", clientParams: malicious }), + ) + const columns = params.get("columns")?.split(",") ?? [] + assert.notInclude(columns, "secret_ciphertext") + assert.include(columns, "config_json") + }) + it("adds Electric Cloud source credentials only when provided", () => { const without = parse(buildUpstreamShapeUrl({ ...base, shape: "dashboards" })).params assert.isNull(without.get("source_id")) diff --git a/apps/web/src/components/alerts/alert-create-page-content.test.ts b/apps/web/src/components/alerts/alert-create-page-content.test.ts new file mode 100644 index 000000000..285efaf37 --- /dev/null +++ b/apps/web/src/components/alerts/alert-create-page-content.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest" + +import { Result } from "@/lib/effect-atom" +import { deriveInitialRuleDraft } from "./alert-create-page-content" + +/** + * The starter-template deep link from the overview empty state. With no ruleId / + * chart / dashboard params, `deriveInitialRuleDraft` reaches the template branch + * before the rules/dashboards results ever matter, so `Result.initial()` stands + * in for both. + */ +const loading = Result.initial() + +describe("deriveInitialRuleDraft — template deep link", () => { + // `low_apdex` differs from the blank defaults on signal, comparator, and + // threshold, so a pass proves the template was applied (not just defaults). + it("pre-applies a known template and skips the first-touch overlay", () => { + const draft = deriveInitialRuleDraft({ + search: { template: "low_apdex" }, + chartContext: undefined, + rulesResult: loading, + dashboardsResult: loading, + }) + + expect(draft.form.signalType).toBe("apdex") + expect(draft.form.comparator).toBe("lt") + expect(draft.form.threshold).toBe("0.8") + expect(draft.form.apdexThresholdMs).toBe("500") + expect(draft.form.name).toBe("Low Apdex score") + expect(draft.showTemplatesInitially).toBe(false) + expect(draft.key).toBe("new:template:low_apdex") + }) + + it("falls through to a blank draft (overlay opens) for an unknown template id", () => { + const draft = deriveInitialRuleDraft({ + search: { template: "not-a-real-template" }, + chartContext: undefined, + rulesResult: loading, + dashboardsResult: loading, + }) + + expect(draft.form.signalType).toBe("error_rate") + expect(draft.form.name).toBe("") + // No serviceName + unknown template → the overlay still leads the flow. + expect(draft.showTemplatesInitially).toBe(true) + expect(draft.key).toBe("new:blank") + }) +}) diff --git a/apps/web/src/components/alerts/alert-create-page-content.tsx b/apps/web/src/components/alerts/alert-create-page-content.tsx index 2841d6d8b..0a35cb9b8 100644 --- a/apps/web/src/components/alerts/alert-create-page-content.tsx +++ b/apps/web/src/components/alerts/alert-create-page-content.tsx @@ -6,12 +6,14 @@ import type { AlertDestinationDocument, AlertRuleDocument, DashboardDocument } f import { AlertCreateFormSurface } from "@/components/alerts/alert-create-form-surface" import { useAutocompleteValuesContext } from "@/hooks/use-autocomplete-values" import { defaultRuleForm, ruleToFormState, type RuleFormState } from "@/lib/alerts/form-utils" +import { ALERT_TEMPLATES, applyTemplate } from "@/lib/alerts/templates" import { decodeAlertChartFromSearchParam, type AlertChartContext } from "@/lib/alerts/widget-chart-param" import { createWidgetAlertPrefill, resolveWidgetAlertPrefill, type WidgetAlertPrefillNotice, } from "@/lib/alerts/widget-prefill" +import { useAlertDestinationsList } from "@/hooks/use-alerts-list" import { Atom, Result, useAtomValue } from "@/lib/effect-atom" import { MapleApiAtomClient } from "@/lib/services/common/atom-client" @@ -21,6 +23,7 @@ type AlertCreateSearchValue = { dashboardId?: string widgetId?: string chart?: string + template?: string } type InitialRuleDraft = { @@ -48,16 +51,13 @@ export function AlertCreatePageContent() { const needsDashboards = !search.ruleId && chartContext == null && Boolean(search.dashboardId || search.widgetId) - const destinationsQueryAtom = MapleApiAtomClient.query("alerts", "listDestinations", { - reactivityKeys: ["alertDestinations"], - }) const rulesQueryAtom = MapleApiAtomClient.query("alerts", "listRules", { reactivityKeys: ["alertRules"], }) const dashboardsQueryAtom = MapleApiAtomClient.query("dashboards", "list", { reactivityKeys: ["dashboards"], }) - const destinationsResult = useAtomValue(destinationsQueryAtom) + const { result: destinationsResult } = useAlertDestinationsList() const rulesResult = useAtomValue(rulesQueryAtom) const dashboardsResult = useAtomValue(needsDashboards ? dashboardsQueryAtom : idleDashboardsAtom) @@ -93,7 +93,7 @@ export function AlertCreatePageContent() { ) } -function deriveInitialRuleDraft({ +export function deriveInitialRuleDraft({ search, chartContext, rulesResult, @@ -213,6 +213,22 @@ function deriveInitialRuleDraft({ } } + // Starter-template deep link from the overview empty state — pre-apply the + // preset and skip the first-touch overlay. An unknown id falls through to the + // blank draft below (overlay still opens). + if (search.template) { + const template = ALERT_TEMPLATES.find((t) => t.id === search.template) + if (template) { + return { + key: `new:template:${template.id}`, + form: applyTemplate(template, base), + prefillNotices: [], + editingRule: null, + showTemplatesInitially: false, + } + } + } + return { key: `new:${search.serviceName ?? "blank"}`, form: base, diff --git a/apps/web/src/components/alerts/overview/alerts-empty-state.tsx b/apps/web/src/components/alerts/overview/alerts-empty-state.tsx new file mode 100644 index 000000000..c645a3fc1 --- /dev/null +++ b/apps/web/src/components/alerts/overview/alerts-empty-state.tsx @@ -0,0 +1,132 @@ +import { Link } from "@tanstack/react-router" + +import { Button } from "@maple/ui/components/ui/button" +import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@maple/ui/components/ui/empty" +import { cn } from "@maple/ui/utils" + +import { PlusIcon } from "@/components/icons" +import { ALERT_TEMPLATES, type AlertTemplate } from "@/lib/alerts/templates" + +/** + * First-run empty state for the alerts overview (zero rules). Replaces the + * generic bell-badge with an operator-console "quiet monitor" readout — a live + * signal held below a not-yet-placed threshold — then surfaces the five real + * starter templates as brand-colored, one-click deep links into a pre-filled + * create form. Non-admins get the readout + a nudge, no create affordances. + * + * The tile hues mirror `BUILTIN_SIGNAL_OPTIONS` on the create form + * (signal-and-threshold-section.tsx) so a template reads with the same color it + * paints on the live chart. Kept here rather than on the pure `ALERT_TEMPLATES` + * module so `templates.ts` stays React-free beyond its display icons. + */ +const TILE_TONE: Record = { + high_error_rate: { glyph: "bg-chart-error/10 text-chart-error", hoverBorder: "hover:border-chart-error/50" }, + slow_p95: { glyph: "bg-chart-p95/10 text-chart-p95", hoverBorder: "hover:border-chart-p95/50" }, + slow_p99: { glyph: "bg-chart-p99/10 text-chart-p99", hoverBorder: "hover:border-chart-p99/50" }, + low_apdex: { glyph: "bg-chart-apdex/10 text-chart-apdex", hoverBorder: "hover:border-chart-apdex/50" }, + throughput_drop: { + glyph: "bg-chart-throughput/10 text-chart-throughput", + hoverBorder: "hover:border-chart-throughput/50", + }, +} + +export function AlertsEmptyState({ isAdmin, serviceName }: { isAdmin: boolean; serviceName?: string }) { + return ( + + + + No rules are watching yet + + {isAdmin + ? "A threshold rule opens an incident the moment a signal crosses it. Start from a common one:" + : "Ask an admin to create the first alert rule."} + + + + {isAdmin && ( +
+
+ {ALERT_TEMPLATES.map((template) => ( + + ))} +
+ +
+ )} +
+ ) +} + +function TemplateTile({ template, serviceName }: { template: AlertTemplate; serviceName?: string }) { + const Icon = template.icon + const tone = TILE_TONE[template.id] + return ( + +
+ + + + {template.title} +
+ {template.summary} + + ) +} + +/** + * The signature graphic: a live signal (muted, with a pulsing leading edge) + * running steadily below a dashed amber threshold that hasn't been placed yet — + * the exact thing a rule adds. The threshold reuses `.infra-ref-line` (dashed + * draw-in) and the leading dot reuses `.infra-pulse`; both idioms already carry + * their own `prefers-reduced-motion` guards from styles.css. + */ +function QuietMonitor() { + return ( +
+ + A steady signal running below an unset alert threshold + {/* Dashed amber threshold — the line a rule would place. */} + + + + {/* Live, steady signal well below the threshold. */} + + {/* Pulsing leading edge — signals are flowing, nothing is watching them. */} + + + +
+ ) +} diff --git a/apps/web/src/components/alerts/overview/alerts-overview-tab.tsx b/apps/web/src/components/alerts/overview/alerts-overview-tab.tsx index c1526d8f5..3ff0c828e 100644 --- a/apps/web/src/components/alerts/overview/alerts-overview-tab.tsx +++ b/apps/web/src/components/alerts/overview/alerts-overview-tab.tsx @@ -1,4 +1,4 @@ -import { Link, useNavigate, useSearch } from "@tanstack/react-router" +import { useNavigate, useSearch } from "@tanstack/react-router" import { Exit } from "effect" import * as AsyncResult from "effect/unstable/reactivity/AsyncResult" import { useEffect, useMemo, useRef, useState } from "react" @@ -11,12 +11,13 @@ import { AlertSegmentedSelect } from "@/components/alerts/alert-segmented-select import { AlertStatStrip } from "@/components/alerts/alert-stat-card" import { AlertTagControls } from "@/components/alerts/alert-tag-controls" import { ActiveIncidentsTable } from "@/components/alerts/overview/active-incidents-table" +import { AlertsEmptyState } from "@/components/alerts/overview/alerts-empty-state" import { AlertsHealthSummary, type AlertsStatusFilter, } from "@/components/alerts/overview/alerts-health-summary" import { RulesOverviewTable } from "@/components/alerts/overview/rules-overview-table" -import { BellIcon, CircleWarningIcon, MagnifierIcon, PlusIcon, XmarkIcon } from "@/components/icons" +import { CircleWarningIcon, MagnifierIcon, XmarkIcon } from "@/components/icons" import { getExitErrorMessage } from "@/lib/alerts/form-utils" import { needsAttention } from "@/lib/alerts/rule-status" import { @@ -25,11 +26,11 @@ import { tagFacets, type TagGroup, } from "@/lib/alerts/tag-grouping" +import { useAlertDestinationsList } from "@/hooks/use-alerts-list" import { Result, useAtomValue } from "@/lib/effect-atom" import { AlertsOverviewModel, type AlertsOverviewReady } from "@/lib/models/alerts-overview-model" import { unitflowRuntime } from "@/lib/models/runtime" import { MapleApiAtomClient } from "@/lib/services/common/atom-client" -import { Button } from "@maple/ui/components/ui/button" import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@maple/ui/components/ui/empty" import { InputGroup, @@ -130,9 +131,7 @@ function AlertsOverviewContent({ } = data const sessionResult = useAtomValue(MapleApiAtomClient.query("auth", "session", {})) - const destinationsResult = useAtomValue( - MapleApiAtomClient.query("alerts", "listDestinations", { reactivityKeys: ["alertDestinations"] }), - ) + const { result: destinationsResult } = useAlertDestinationsList() const destinations = Result.builder(destinationsResult) .onSuccess((response) => [...response.destinations] as AlertDestinationDocument[]) @@ -327,27 +326,7 @@ function AlertsOverviewContent({ {filteredRules.length === 0 && rules.length === 0 ? ( - - - - - - No alert rules - - Create a threshold rule to open incidents for latency, error rate, throughput, - Apdex, or exact metrics. - - - {isAdmin && ( - - )} - + ) : filteredRules.length === 0 ? ( diff --git a/apps/web/src/components/alerts/overview/settings-tab.tsx b/apps/web/src/components/alerts/overview/settings-tab.tsx index 3ea43706f..2c1112041 100644 --- a/apps/web/src/components/alerts/overview/settings-tab.tsx +++ b/apps/web/src/components/alerts/overview/settings-tab.tsx @@ -21,6 +21,7 @@ import { groupDeliveryEventsByDay, type DestinationFormState, } from "@/lib/alerts/form-utils" +import { useAlertDestinationsList } from "@/hooks/use-alerts-list" import { MapleApiAtomClient } from "@/lib/services/common/atom-client" import { Result, useAtomSet, useAtomValue } from "@/lib/effect-atom" import { Badge } from "@maple/ui/components/ui/badge" @@ -183,9 +184,7 @@ export function useDestinationManager(): DestinationManager { * "Add destination" action drives the same dialog. */ export function AlertsSettingsTab({ manager, isAdmin }: { manager: DestinationManager; isAdmin: boolean }) { - const destinationsResult = useAtomValue( - MapleApiAtomClient.query("alerts", "listDestinations", { reactivityKeys: ["alertDestinations"] }), - ) + const { result: destinationsResult } = useAlertDestinationsList() const deliveryEventsResult = useAtomValue( MapleApiAtomClient.query("alerts", "listDeliveryEvents", { reactivityKeys: ["alertDeliveryEvents"] }), ) diff --git a/apps/web/src/components/settings/escalation-policy-section.tsx b/apps/web/src/components/settings/escalation-policy-section.tsx index 3e15e4c32..c9aec6f01 100644 --- a/apps/web/src/components/settings/escalation-policy-section.tsx +++ b/apps/web/src/components/settings/escalation-policy-section.tsx @@ -7,6 +7,7 @@ import { Schema } from "effect" import { Result, useAtomRefresh, useAtomSet, useAtomValue } from "@/lib/effect-atom" import { MapleApiAtomClient } from "@/lib/services/common/atom-client" +import { useAlertDestinationsList } from "@/hooks/use-alerts-list" import { IssueEscalationPolicyRule, IssueEscalationPolicyUpsertRequest, @@ -59,9 +60,7 @@ export function EscalationPolicySection({ isAdmin }: { isAdmin: boolean }) { const policyResult = useAtomValue(policyQueryAtom) const refreshPolicy = useAtomRefresh(policyQueryAtom) - const destinationsQueryAtom = MapleApiAtomClient.query("alerts", "listDestinations", {}) - const destinationsResult = useAtomValue(destinationsQueryAtom) - const refreshDestinations = useAtomRefresh(destinationsQueryAtom) + const { result: destinationsResult, refresh: refreshDestinations } = useAlertDestinationsList() const upsertMutation = useAtomSet(MapleApiAtomClient.mutation("errors", "upsertEscalationPolicy"), { mode: "promiseExit", diff --git a/apps/web/src/components/settings/scrape-targets-section.tsx b/apps/web/src/components/settings/scrape-targets-section.tsx index eda90fd2c..5c98e54b1 100644 --- a/apps/web/src/components/settings/scrape-targets-section.tsx +++ b/apps/web/src/components/settings/scrape-targets-section.tsx @@ -16,6 +16,8 @@ import { useState, type KeyboardEvent, type ReactNode } from "react" import { Exit, Schema } from "effect" import { toast } from "sonner" +import { useScrapeTargetChecks } from "@/hooks/use-scrape-target-checks" + import { Alert, AlertDescription, AlertTitle } from "@maple/ui/components/ui/alert" import { AlertDialog, @@ -846,13 +848,7 @@ function ScrapeTargetRow({ onEdit: (target: ScrapeTarget) => void onDelete: (target: ScrapeTarget) => void }) { - const latestCheckResult = useAtomValue( - MapleApiAtomClient.query("scrapeTargets", "listChecks", { - params: { targetId: target.id }, - query: { limit: 1 }, - reactivityKeys: ["scrapeTargetChecks", target.id, "latest"], - }), - ) + const { result: latestCheckResult } = useScrapeTargetChecks(target.id, 1) const latestCheck = checksFromResult(latestCheckResult).at(0) ?? null const status = scheduledStatus(target, latestCheck, Result.isInitial(latestCheckResult)) @@ -1009,12 +1005,7 @@ function ScrapeTargetDetails({ onEdit: (target: ScrapeTarget) => void onDelete: (target: ScrapeTarget) => void }) { - const checksQueryAtom = MapleApiAtomClient.query("scrapeTargets", "listChecks", { - params: { targetId: target.id }, - query: { limit: 20 }, - reactivityKeys: ["scrapeTargetChecks", target.id], - }) - const checksResult = useAtomValue(checksQueryAtom) + const { result: checksResult } = useScrapeTargetChecks(target.id, 20) const checks = checksFromResult(checksResult) const latestCheck = checks.at(0) ?? null const status = scheduledStatus(target, latestCheck, Result.isInitial(checksResult)) diff --git a/apps/web/src/hooks/use-alerts-list.ts b/apps/web/src/hooks/use-alerts-list.ts index 43d6243f1..e020da47a 100644 --- a/apps/web/src/hooks/use-alerts-list.ts +++ b/apps/web/src/hooks/use-alerts-list.ts @@ -1,9 +1,14 @@ -import { AlertIncidentsListResponse, AlertRulesListResponse } from "@maple/domain/http" +import { + AlertDestinationsListResponse, + AlertIncidentsListResponse, + AlertRulesListResponse, +} from "@maple/domain/http" import { useLiveQuery } from "@tanstack/react-db" import { useMemo } from "react" import { Result } from "@/lib/effect-atom" import { buildRuleStatesByRuleId, + rowToAlertDestinationDocument, rowToAlertIncidentDocument, rowToAlertRuleDocument, } from "@/lib/collections/alerts" @@ -32,6 +37,11 @@ export interface AlertIncidentsListHook { readonly refresh: () => void } +export interface AlertDestinationsListHook { + readonly result: Result.Result + readonly refresh: () => void +} + const noop = () => {} export function useAlertRulesList(): AlertRulesListHook { @@ -88,3 +98,27 @@ export function useAlertIncidentsList(): AlertIncidentsListHook { return { result, refresh: noop } } + +export function useAlertDestinationsList(): AlertDestinationsListHook { + const orgKey = useActiveOrgId() ?? "pending" + const generation = useCollectionsGeneration() + const collection = useMemo( + () => getOrgCollections(orgKey).alertDestinations, + // eslint-disable-next-line react-hooks/exhaustive-deps + [orgKey, generation], + ) + + const { data: rows, isLoading } = useLiveQuery( + // Match the server's `listDestinations` ordering (desc updatedAt). + (q) => q.from({ d: collection }).orderBy(({ d }) => d.updated_at, "desc"), + [collection], + ) + + const result = useMemo>(() => { + if (isLoading && (rows?.length ?? 0) === 0) return Result.initial(true) + const destinations = (rows ?? []).map(rowToAlertDestinationDocument) + return Result.success(new AlertDestinationsListResponse({ destinations })) + }, [rows, isLoading]) + + return { result, refresh: noop } +} diff --git a/apps/web/src/hooks/use-scrape-target-checks.ts b/apps/web/src/hooks/use-scrape-target-checks.ts new file mode 100644 index 000000000..b2c7ffeab --- /dev/null +++ b/apps/web/src/hooks/use-scrape-target-checks.ts @@ -0,0 +1,52 @@ +import { ScrapeTargetChecksListResponse } from "@maple/domain/http" +import { useLiveQuery } from "@tanstack/react-db" +import { useMemo } from "react" +import { rowToScrapeTargetCheckDocument } from "@/lib/collections/scrape-targets" +import { + getOrgCollections, + useActiveOrgId, + useCollectionsGeneration, +} from "@/lib/collections/org-collections" +import { Result } from "@/lib/effect-atom" + +type ListError = { readonly message: string } + +export interface ScrapeTargetChecksHook { + readonly result: Result.Result + readonly refresh: () => void +} + +const noop = () => {} + +/** + * Live `scrape_target_checks` for one target, synced via Electric. Mirrors the + * server's `listChecks` (newest-first, capped at `limit`) but stays current + * without polling — replaces `MapleApiAtomClient.query("scrapeTargets", + * "listChecks", …)`. The collection holds every check for the org; we filter to + * the target client-side (bounded to 24h / 10k-per-target by server pruning), + * exactly like `useAlertRuleStates` filters by rule id. + */ +export function useScrapeTargetChecks(targetId: string, limit: number): ScrapeTargetChecksHook { + const orgKey = useActiveOrgId() ?? "pending" + const generation = useCollectionsGeneration() + const collection = useMemo( + () => getOrgCollections(orgKey).scrapeTargetChecks, + // eslint-disable-next-line react-hooks/exhaustive-deps + [orgKey, generation], + ) + + const { data: rows, isLoading } = useLiveQuery((q) => q.from({ c: collection }), [collection]) + + const result = useMemo>(() => { + if (isLoading && (rows?.length ?? 0) === 0) return Result.initial(true) + const checks = (rows ?? []) + .filter((row) => row.target_id === targetId) + // ISO timestamps order lexicographically; newest first, matching the server. + .sort((a, b) => (a.checked_at < b.checked_at ? 1 : a.checked_at > b.checked_at ? -1 : 0)) + .slice(0, limit) + .map(rowToScrapeTargetCheckDocument) + return Result.success(new ScrapeTargetChecksListResponse({ checks })) + }, [rows, isLoading, targetId, limit]) + + return { result, refresh: noop } +} diff --git a/apps/web/src/lib/collections/alerts.test.ts b/apps/web/src/lib/collections/alerts.test.ts index 3650f47a0..f1508d672 100644 --- a/apps/web/src/lib/collections/alerts.test.ts +++ b/apps/web/src/lib/collections/alerts.test.ts @@ -6,10 +6,12 @@ import { vi } from "vitest" vi.mock("@/lib/registry", () => ({ mapleRuntime: {} })) import { + type AlertDestinationRow, type AlertIncidentRow, type AlertRuleRow, type AlertRuleStateRow, buildRuleStatesByRuleId, + rowToAlertDestinationDocument, rowToAlertIncidentDocument, rowToAlertRuleDocument, } from "./alerts" @@ -179,3 +181,44 @@ describe("rowToAlertIncidentDocument", () => { assert.strictEqual(doc.firstTriggeredAt, "2026-07-04T00:00:00.000Z") }) }) + +describe("rowToAlertDestinationDocument", () => { + const base: AlertDestinationRow = { + id: DEST_ID, + org_id: "org_1", + name: "Ops Slack", + type: "slack", + enabled: true, + // Only the public config the browser renders — no secrets (those live in + // the excluded encrypted columns, which the shape never projects). + config_json: { summary: "Slack incoming webhook", channelLabel: "#ops" }, + last_tested_at: "2026-07-04T00:00:00.000Z", + last_test_error: null, + created_at: "2026-06-01T00:00:00.000Z", + updated_at: "2026-07-04T00:00:00.000Z", + } + + it("maps a raw alert_destinations row and derives the public config", () => { + const doc = rowToAlertDestinationDocument(base) + assert.strictEqual(doc.id, DEST_ID) + assert.strictEqual(doc.name, "Ops Slack") + assert.strictEqual(doc.type, "slack") + assert.strictEqual(doc.enabled, true) + assert.strictEqual(doc.summary, "Slack incoming webhook") + assert.strictEqual(doc.channelLabel, "#ops") + assert.strictEqual(doc.lastTestedAt, "2026-07-04T00:00:00.000Z") + assert.strictEqual(doc.lastTestError, null) + assert.strictEqual(doc.createdAt, "2026-06-01T00:00:00.000Z") + }) + + it("falls back to the server's invalid-config summary when config_json is unusable", () => { + const doc = rowToAlertDestinationDocument({ ...base, config_json: { nope: true } }) + assert.strictEqual(doc.summary, "Invalid destination config") + assert.strictEqual(doc.channelLabel, null) + }) + + it("leaves lastTestedAt null when the destination was never tested", () => { + const doc = rowToAlertDestinationDocument({ ...base, last_tested_at: null }) + assert.strictEqual(doc.lastTestedAt, null) + }) +}) diff --git a/apps/web/src/lib/collections/alerts.ts b/apps/web/src/lib/collections/alerts.ts index 32560a0c0..d0e7c332c 100644 --- a/apps/web/src/lib/collections/alerts.ts +++ b/apps/web/src/lib/collections/alerts.ts @@ -1,6 +1,8 @@ import { AlertComparator, + AlertDestinationDocument, AlertDestinationId, + AlertDestinationType, AlertEventType, AlertGroupBy, AlertIncidentDocument, @@ -28,6 +30,8 @@ const asUserId = Schema.decodeUnknownSync(UserId) const asErrorIssueId = Schema.decodeUnknownSync(ErrorIssueId) const decodeDestinationId = Schema.decodeUnknownSync(AlertDestinationId) +const asDestinationType = Schema.decodeUnknownSync(AlertDestinationType) + const asSeverity = Schema.decodeUnknownSync(AlertSeverity) const asSignalType = Schema.decodeUnknownSync(AlertSignalType) const asComparator = Schema.decodeUnknownSync(AlertComparator) @@ -293,6 +297,79 @@ export const createAlertIncidentsCollection = (orgId: string) => getKey: (row) => row.id, }) +// --------------------------------------------------------------------------- +// alert_destinations +// --------------------------------------------------------------------------- + +/** + * Identity row schema for the `alert_destinations` shape. The shape is + * column-restricted server-side (the encrypted `secret_*` columns are dropped — + * see the proxy whitelist), so this struct intentionally lists only the synced + * columns. `config_json` carries ONLY public config (summary / channel label / + * hazel metadata); the webhook secrets live in the excluded encrypted columns. + */ +export const AlertDestinationRowSchema = Schema.Struct({ + id: Schema.String, + org_id: Schema.String, + name: Schema.String, + type: Schema.String, + enabled: Schema.Boolean, + config_json: Schema.Unknown, + last_tested_at: Schema.NullOr(Schema.String), + last_test_error: Schema.NullOr(Schema.String), + created_at: Schema.String, + updated_at: Schema.String, +}) +export type AlertDestinationRow = typeof AlertDestinationRowSchema.Type + +/** + * The public projection of a destination's `config_json` the browser renders — + * mirrors the server's `DestinationPublicConfigSchema` (summary + channelLabel; + * the hazel-* fields are excess and ignored here). The server stringifies the + * jsonb before decoding; Electric's default parser already gives us the parsed + * object, so we decode it directly. + */ +const AlertDestinationPublicConfig = Schema.Struct({ + summary: Schema.String, + channelLabel: Schema.NullOr(Schema.String), +}) +const decodeDestinationPublicConfig = Schema.decodeUnknownOption(AlertDestinationPublicConfig) + +/** + * Decodes a raw `alert_destinations` row into the domain + * {@link AlertDestinationDocument}, mirroring `rowToDestinationDocument` + + * `safeParsePublicConfig` in AlertsService.ts (invalid config → the same + * "Invalid destination config" fallback the server uses). + */ +export const rowToAlertDestinationDocument = (row: AlertDestinationRow): AlertDestinationDocument => { + const publicConfig = Option.getOrElse(decodeDestinationPublicConfig(row.config_json), () => ({ + summary: "Invalid destination config", + channelLabel: null, + })) + return new AlertDestinationDocument({ + id: decodeDestinationId(row.id), + name: row.name, + type: asDestinationType(row.type), + enabled: row.enabled, + summary: publicConfig.summary, + channelLabel: publicConfig.channelLabel, + lastTestedAt: row.last_tested_at != null ? decodeIso(row.last_tested_at) : null, + lastTestError: row.last_test_error, + createdAt: decodeIso(row.created_at), + updatedAt: decodeIso(row.updated_at), + }) +} + +export const createAlertDestinationsCollection = (orgId: string) => + createSyncedCollection({ + shape: "alert_destinations", + orgId, + schema: AlertDestinationRowSchema, + parser: timestamptzParser, + getKey: (row) => row.id, + }) + export type AlertRulesCollection = ReturnType export type AlertRuleStatesCollection = ReturnType export type AlertIncidentsCollection = ReturnType +export type AlertDestinationsCollection = ReturnType diff --git a/apps/web/src/lib/collections/org-collections.test.ts b/apps/web/src/lib/collections/org-collections.test.ts index 310d1e918..78df8cc09 100644 --- a/apps/web/src/lib/collections/org-collections.test.ts +++ b/apps/web/src/lib/collections/org-collections.test.ts @@ -38,6 +38,7 @@ vi.mock("./alerts", () => ({ createAlertRulesCollection: collectionStub, createAlertRuleStatesCollection: collectionStub, createAlertIncidentsCollection: collectionStub, + createAlertDestinationsCollection: collectionStub, })) vi.mock("./dashboards", () => ({ createDashboardsCollection: collectionStub })) vi.mock("./errors", () => ({ @@ -45,6 +46,7 @@ vi.mock("./errors", () => ({ createActorsCollection: collectionStub, createOpenErrorIncidentsCollection: collectionStub, })) +vi.mock("./scrape-targets", () => ({ createScrapeTargetChecksCollection: collectionStub })) // Each test wants isolated module state (generation counter + heal budget), so // re-import a fresh copy. Importing registry in the same epoch first hands back diff --git a/apps/web/src/lib/collections/org-collections.ts b/apps/web/src/lib/collections/org-collections.ts index 01e1fed14..a9b0a3af7 100644 --- a/apps/web/src/lib/collections/org-collections.ts +++ b/apps/web/src/lib/collections/org-collections.ts @@ -3,9 +3,11 @@ import { useSyncExternalStore } from "react" import { mapleRuntime } from "@/lib/registry" import { getActiveOrgId, subscribeActiveOrgId } from "@/lib/services/common/auth-headers" import { + createAlertDestinationsCollection, createAlertIncidentsCollection, createAlertRulesCollection, createAlertRuleStatesCollection, + type AlertDestinationsCollection, type AlertIncidentsCollection, type AlertRulesCollection, type AlertRuleStatesCollection, @@ -19,6 +21,7 @@ import { type ErrorIssuesCollection, type OpenErrorIncidentsCollection, } from "./errors" +import { createScrapeTargetChecksCollection, type ScrapeTargetChecksCollection } from "./scrape-targets" /** * The set of ElectricSQL-synced collections for one org. Collections are @@ -33,9 +36,11 @@ export type OrgCollections = { readonly alertRules: AlertRulesCollection readonly alertRuleStates: AlertRuleStatesCollection readonly alertIncidents: AlertIncidentsCollection + readonly alertDestinations: AlertDestinationsCollection readonly errorIssues: ErrorIssuesCollection readonly actors: ActorsCollection readonly openErrorIncidents: OpenErrorIncidentsCollection + readonly scrapeTargetChecks: ScrapeTargetChecksCollection } // Single live set at a time — the app shows one org at a time, and recreating on @@ -189,9 +194,11 @@ const scheduleOrgCollectionsCleanup = (collections: OrgCollections): void => { cleanupCollectionWhenIdle(collections.alertRules) cleanupCollectionWhenIdle(collections.alertRuleStates) cleanupCollectionWhenIdle(collections.alertIncidents) + cleanupCollectionWhenIdle(collections.alertDestinations) cleanupCollectionWhenIdle(collections.errorIssues) cleanupCollectionWhenIdle(collections.actors) cleanupCollectionWhenIdle(collections.openErrorIncidents) + cleanupCollectionWhenIdle(collections.scrapeTargetChecks) } // Tracks the last org we resolved collections for, independent of `current` @@ -215,9 +222,11 @@ export const getOrgCollections = (orgId: string): OrgCollections => { alertRules: createAlertRulesCollection(orgId), alertRuleStates: createAlertRuleStatesCollection(orgId), alertIncidents: createAlertIncidentsCollection(orgId), + alertDestinations: createAlertDestinationsCollection(orgId), errorIssues: createErrorIssuesCollection(orgId), actors: createActorsCollection(orgId), openErrorIncidents: createOpenErrorIncidentsCollection(orgId), + scrapeTargetChecks: createScrapeTargetChecksCollection(orgId), } // Tear down the previous org's shape streams after swapping so an in-flight // read never resolves against the wrong org. Deferred (see diff --git a/apps/web/src/lib/collections/scrape-targets.test.ts b/apps/web/src/lib/collections/scrape-targets.test.ts new file mode 100644 index 000000000..4031ba7b6 --- /dev/null +++ b/apps/web/src/lib/collections/scrape-targets.test.ts @@ -0,0 +1,53 @@ +import { assert, describe, it } from "@effect/vitest" +import { vi } from "vitest" + +// The mapper is pure; stub the registry so importing the collection module +// doesn't spin up the ManagedRuntime / atom-registry side effects. +vi.mock("@/lib/registry", () => ({ mapleRuntime: {} })) + +import { type ScrapeTargetCheckRow, rowToScrapeTargetCheckDocument } from "./scrape-targets" + +const base: ScrapeTargetCheckRow = { + id: 42, + target_id: "target_1", + org_id: "org_1", + sub_target_key: "", + checked_at: "2026-07-04T00:00:00.000Z", + error: null, + duration_ms: 1500, + samples_scraped: 120, + samples_post_relabel: 100, +} + +describe("rowToScrapeTargetCheckDocument", () => { + it("maps a successful check and converts ms → seconds", () => { + const doc = rowToScrapeTargetCheckDocument(base) + assert.strictEqual(doc.timestamp, "2026-07-04T00:00:00.000Z") + assert.strictEqual(doc.success, true) + assert.strictEqual(doc.subTargetKey, null) + assert.strictEqual(doc.durationSeconds, 1.5) + assert.strictEqual(doc.samplesScraped, 120) + assert.strictEqual(doc.samplesPostMetricRelabeling, 100) + assert.strictEqual(doc.message, null) + }) + + it("maps a failed check: error → message + success false", () => { + const doc = rowToScrapeTargetCheckDocument({ ...base, error: "connection refused" }) + assert.strictEqual(doc.success, false) + assert.strictEqual(doc.message, "connection refused") + }) + + it("surfaces a non-empty sub_target_key and preserves null metrics", () => { + const doc = rowToScrapeTargetCheckDocument({ + ...base, + sub_target_key: "branch:main", + duration_ms: null, + samples_scraped: null, + samples_post_relabel: null, + }) + assert.strictEqual(doc.subTargetKey, "branch:main") + assert.strictEqual(doc.durationSeconds, null) + assert.strictEqual(doc.samplesScraped, null) + assert.strictEqual(doc.samplesPostMetricRelabeling, null) + }) +}) diff --git a/apps/web/src/lib/collections/scrape-targets.ts b/apps/web/src/lib/collections/scrape-targets.ts new file mode 100644 index 000000000..2e13f71ce --- /dev/null +++ b/apps/web/src/lib/collections/scrape-targets.ts @@ -0,0 +1,58 @@ +import { IsoDateTimeString, ScrapeTargetCheckResponse } from "@maple/domain/http" +import { Schema } from "effect" +import { createSyncedCollection, timestamptzParser } from "./shape-fetch" + +const decodeIso = Schema.decodeUnknownSync(IsoDateTimeString) + +// --------------------------------------------------------------------------- +// scrape_target_checks +// --------------------------------------------------------------------------- + +/** + * Identity row schema for the `scrape_target_checks` shape — one row per + * scheduled scrape attempt (uptime-probe history). The table is already pruned + * server-side to a 24h window + a 10k-per-target cap (ScrapeTargetsService. + * pruneChecks), so the whole per-org shape stays bounded. `id` is an int4 + * identity (Electric's default parser decodes int4 → number). No secrets. + */ +export const ScrapeTargetCheckRowSchema = Schema.Struct({ + id: Schema.Number, + target_id: Schema.String, + org_id: Schema.String, + sub_target_key: Schema.String, + checked_at: Schema.String, + error: Schema.NullOr(Schema.String), + duration_ms: Schema.NullOr(Schema.Number), + samples_scraped: Schema.NullOr(Schema.Number), + samples_post_relabel: Schema.NullOr(Schema.Number), +}) +export type ScrapeTargetCheckRow = typeof ScrapeTargetCheckRowSchema.Type + +/** + * Decodes a raw `scrape_target_checks` row into the domain + * {@link ScrapeTargetCheckResponse}, mirroring the transform in the server's + * `listChecks` route handler (scrape-targets.http.ts): `error === null` → + * `success`, empty `sub_target_key` → null, `duration_ms / 1000` → seconds, + * `samples_post_relabel` → `samplesPostMetricRelabeling`. + */ +export const rowToScrapeTargetCheckDocument = (row: ScrapeTargetCheckRow): ScrapeTargetCheckResponse => + new ScrapeTargetCheckResponse({ + timestamp: decodeIso(row.checked_at), + success: row.error === null, + subTargetKey: row.sub_target_key === "" ? null : row.sub_target_key, + durationSeconds: row.duration_ms === null ? null : row.duration_ms / 1000, + samplesScraped: row.samples_scraped, + samplesPostMetricRelabeling: row.samples_post_relabel, + message: row.error, + }) + +export const createScrapeTargetChecksCollection = (orgId: string) => + createSyncedCollection({ + shape: "scrape_target_checks", + orgId, + schema: ScrapeTargetCheckRowSchema, + parser: timestamptzParser, + getKey: (row) => String(row.id), + }) + +export type ScrapeTargetChecksCollection = ReturnType diff --git a/apps/web/src/routes/alerts/$ruleId.tsx b/apps/web/src/routes/alerts/$ruleId.tsx index 1abbf3bf4..1cad28830 100644 --- a/apps/web/src/routes/alerts/$ruleId.tsx +++ b/apps/web/src/routes/alerts/$ruleId.tsx @@ -41,7 +41,11 @@ import { type AlertIncidentDocument, type AlertRuleDocument, } from "@maple/domain/http" -import { useAlertIncidentsList, useAlertRulesList } from "@/hooks/use-alerts-list" +import { + useAlertDestinationsList, + useAlertIncidentsList, + useAlertRulesList, +} from "@/hooks/use-alerts-list" import { AiTriageCard } from "@/components/ai-triage/ai-triage-card" import { AlertChatSheet } from "@/components/alerts/alert-chat-sheet" import { toAlertContext, type AlertContext } from "@/components/chat/alert-context" @@ -121,9 +125,7 @@ function RuleDetailContent() { const { result: rulesResult, refresh: refreshRules } = useAlertRulesList() const { result: incidentsResult, refresh: refreshIncidents } = useAlertIncidentsList() const ruleStates = useAlertRuleStates(ruleId) - const destinationsResult = useAtomValue( - MapleApiAtomClient.query("alerts", "listDestinations", { reactivityKeys: ["alertDestinations"] }), - ) + const { result: destinationsResult } = useAlertDestinationsList() const deliveryEventsResult = useAtomValue( MapleApiAtomClient.query("alerts", "listDeliveryEvents", { reactivityKeys: ["alertDeliveryEvents"], diff --git a/apps/web/src/routes/alerts/create.tsx b/apps/web/src/routes/alerts/create.tsx index 6afe5c624..b41c2ebc4 100644 --- a/apps/web/src/routes/alerts/create.tsx +++ b/apps/web/src/routes/alerts/create.tsx @@ -7,6 +7,8 @@ import { AlertCreatePageRoot } from "@/components/alerts/alert-create-page-root" const AlertCreateSearch = Schema.Struct({ serviceName: Schema.optional(Schema.String), ruleId: Schema.optional(Schema.String), + /** Starter-template id from the overview empty state — pre-applies that preset. */ + template: Schema.optional(Schema.String), /** Set by the "Create alert" action on a dashboard chart widget. */ dashboardId: Schema.optional(Schema.String), widgetId: Schema.optional(Schema.String), diff --git a/apps/web/src/routes/alerts/index.tsx b/apps/web/src/routes/alerts/index.tsx index 4c1a99076..33feb2715 100644 --- a/apps/web/src/routes/alerts/index.tsx +++ b/apps/web/src/routes/alerts/index.tsx @@ -6,6 +6,7 @@ import { AlertsOverviewTab } from "@/components/alerts/overview/alerts-overview- import { AlertsSettingsTab, useDestinationManager } from "@/components/alerts/overview/settings-tab" import { DashboardLayout } from "@/components/layout/dashboard-layout" import { PlusIcon } from "@/components/icons" +import { useAlertDestinationsList } from "@/hooks/use-alerts-list" import { MapleApiAtomClient } from "@/lib/services/common/atom-client" import { BooleanFromStringParam, OptionalStringArrayParam } from "@/lib/search-params" import { Result, useAtomValue } from "@/lib/effect-atom" @@ -44,9 +45,7 @@ function AlertsPage() { // Session + destinations back the header action only; the tabs own the rest // of their data (the atoms are shared, so this costs no extra requests). const sessionResult = useAtomValue(MapleApiAtomClient.query("auth", "session", {})) - const destinationsResult = useAtomValue( - MapleApiAtomClient.query("alerts", "listDestinations", { reactivityKeys: ["alertDestinations"] }), - ) + const { result: destinationsResult } = useAlertDestinationsList() const isAdmin = Result.builder(sessionResult) .onSuccess((session) => session.roles.some((role) => role === "root" || role === "org:admin")) .orElse(() => false) diff --git a/packages/db/drizzle/0011_electric_publication_wave1.sql b/packages/db/drizzle/0011_electric_publication_wave1.sql new file mode 100644 index 000000000..603a36b3c --- /dev/null +++ b/packages/db/drizzle/0011_electric_publication_wave1.sql @@ -0,0 +1,33 @@ +-- ElectricSQL sync — Wave 1 tables. +-- +-- Adds two control-plane tables to the existing `electric_publication_default` +-- publication + REPLICA IDENTITY FULL so Electric can tail them as HTTP shapes: +-- * alert_destinations (finishes the alerts settings surface) +-- * scrape_target_checks (uptime-probe history; already 24h/10k-bounded server-side) +-- +-- See 0009_electric_publication for the rationale (manual publishing on +-- PlanetScale, and why REPLICA IDENTITY FULL is required — composite-key deletes +-- and WHERE-exit deletes). `alert_destinations` is served column-restricted by the +-- shape proxy (its encrypted `secret_*` columns are never projected to the +-- browser); the publication still carries the full row via REPLICA IDENTITY FULL, +-- and the proxy pins the column projection on the shape request, so the client +-- only ever receives the whitelisted columns. +-- +-- Wrapped in the same DO/EXCEPTION guard as 0009 so the embedded PGlite test path +-- (readBundledMigrationsSql → single pglite.exec of every *.sql) never aborts on +-- an unsupported statement. On real Postgres it runs exactly once (drizzle tracks +-- applied migrations) and swallowing duplicates keeps it idempotent. +DO $$ +BEGIN + ALTER TABLE "alert_destinations" REPLICA IDENTITY FULL; + ALTER TABLE "scrape_target_checks" REPLICA IDENTITY FULL; + + ALTER PUBLICATION electric_publication_default ADD TABLE + "alert_destinations", + "scrape_target_checks"; +EXCEPTION + WHEN duplicate_object THEN + RAISE NOTICE 'electric publication already includes the wave-1 tables, skipping'; + WHEN OTHERS THEN + RAISE NOTICE 'electric publication wave-1 migration skipped: %', SQLERRM; +END $$; diff --git a/packages/db/drizzle/meta/0011_snapshot.json b/packages/db/drizzle/meta/0011_snapshot.json new file mode 100644 index 000000000..d95785eda --- /dev/null +++ b/packages/db/drizzle/meta/0011_snapshot.json @@ -0,0 +1,5800 @@ +{ + "id": "8c4b199f-f711-4b87-961a-1672eb536e58", + "prevId": "346220d3-f1f9-494e-bd78-7f916ee31352", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ai_triage_runs": { + "name": "ai_triage_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "incident_kind": { + "name": "incident_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "incident_id": { + "name": "incident_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "context_json": { + "name": "context_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "ai_triage_runs_incident_idx": { + "name": "ai_triage_runs_incident_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "incident_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "incident_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_triage_runs_org_issue_idx": { + "name": "ai_triage_runs_org_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ai_triage_runs_org_created_idx": { + "name": "ai_triage_runs_org_created_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_triage_settings": { + "name": "ai_triage_settings", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "max_runs_per_day": { + "name": "max_runs_per_day", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_delivery_events": { + "name": "alert_delivery_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "incident_id": { + "name": "incident_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rule_id": { + "name": "rule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "destination_id": { + "name": "destination_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_key": { + "name": "delivery_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt_number": { + "name": "attempt_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider_message": { + "name": "provider_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_reference": { + "name": "provider_reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_code": { + "name": "response_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "alert_delivery_events_org_idx": { + "name": "alert_delivery_events_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_delivery_events_org_incident_idx": { + "name": "alert_delivery_events_org_incident_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "incident_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_delivery_events_due_idx": { + "name": "alert_delivery_events_due_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_delivery_events_claim_idx": { + "name": "alert_delivery_events_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "claim_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_delivery_events_delivery_attempt_idx": { + "name": "alert_delivery_events_delivery_attempt_idx", + "columns": [ + { + "expression": "delivery_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_destinations": { + "name": "alert_destinations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "secret_ciphertext": { + "name": "secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_iv": { + "name": "secret_iv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_tag": { + "name": "secret_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_tested_at": { + "name": "last_tested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_error": { + "name": "last_test_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "alert_destinations_org_idx": { + "name": "alert_destinations_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_destinations_org_enabled_idx": { + "name": "alert_destinations_org_enabled_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_destinations_org_name_idx": { + "name": "alert_destinations_org_name_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_incidents": { + "name": "alert_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "incident_key": { + "name": "incident_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_name": { + "name": "rule_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signal_type": { + "name": "signal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "comparator": { + "name": "comparator", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold": { + "name": "threshold", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "threshold_upper": { + "name": "threshold_upper", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "first_triggered_at": { + "name": "first_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_observed_value": { + "name": "last_observed_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "last_sample_count": { + "name": "last_sample_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_delivered_event_type": { + "name": "last_delivered_event_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_notified_at": { + "name": "last_notified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_issue_id": { + "name": "error_issue_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "alert_incidents_org_idx": { + "name": "alert_incidents_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_incidents_org_status_idx": { + "name": "alert_incidents_org_status_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_incidents_org_rule_idx": { + "name": "alert_incidents_org_rule_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_incidents_org_issue_idx": { + "name": "alert_incidents_org_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "error_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_incidents_incident_key_idx": { + "name": "alert_incidents_incident_key_idx", + "columns": [ + { + "expression": "incident_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rule_states": { + "name": "alert_rule_states", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'__total__'" + }, + "consecutive_breaches": { + "name": "consecutive_breaches", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "consecutive_healthy": { + "name": "consecutive_healthy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_status": { + "name": "last_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_value": { + "name": "last_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "last_sample_count": { + "name": "last_sample_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "alert_rule_states_org_idx": { + "name": "alert_rule_states_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "alert_rule_states_org_id_rule_id_group_key_pk": { + "name": "alert_rule_states_org_id_rule_id_group_key_pk", + "columns": [ + "org_id", + "rule_id", + "group_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.alert_rules": { + "name": "alert_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notification_template_json": { + "name": "notification_template_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_names_json": { + "name": "service_names_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "exclude_service_names_json": { + "name": "exclude_service_names_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tags_json": { + "name": "tags_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "signal_type": { + "name": "signal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "comparator": { + "name": "comparator", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "threshold": { + "name": "threshold", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "threshold_upper": { + "name": "threshold_upper", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "window_minutes": { + "name": "window_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "minimum_sample_count": { + "name": "minimum_sample_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "consecutive_breaches_required": { + "name": "consecutive_breaches_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "consecutive_healthy_required": { + "name": "consecutive_healthy_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2 + }, + "renotify_interval_minutes": { + "name": "renotify_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "metric_name": { + "name": "metric_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metric_type": { + "name": "metric_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metric_aggregation": { + "name": "metric_aggregation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "apdex_threshold_ms": { + "name": "apdex_threshold_ms", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "query_builder_draft_json": { + "name": "query_builder_draft_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "raw_query_sql": { + "name": "raw_query_sql", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_by": { + "name": "group_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "destination_ids_json": { + "name": "destination_ids_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "query_spec_json": { + "name": "query_spec_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reducer": { + "name": "reducer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sample_count_strategy": { + "name": "sample_count_strategy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "no_data_behavior": { + "name": "no_data_behavior", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_scheduled_at": { + "name": "last_scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "alert_rules_org_idx": { + "name": "alert_rules_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_rules_org_enabled_idx": { + "name": "alert_rules_org_enabled_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "alert_rules_org_name_idx": { + "name": "alert_rules_org_name_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.anomaly_detector_settings": { + "name": "anomaly_detector_settings", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sensitivity": { + "name": "sensitivity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "muted_signals_json": { + "name": "muted_signals_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "last_tick_at": { + "name": "last_tick_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.anomaly_detector_states": { + "name": "anomaly_detector_states", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detector_key": { + "name": "detector_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signal_type": { + "name": "signal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_env": { + "name": "deployment_env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "fingerprint_hash": { + "name": "fingerprint_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consecutive_breaches": { + "name": "consecutive_breaches", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "consecutive_healthy": { + "name": "consecutive_healthy", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_status": { + "name": "last_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_value": { + "name": "last_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "baseline_median": { + "name": "baseline_median", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "last_sample_count": { + "name": "last_sample_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "open_incident_id": { + "name": "open_incident_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_resolved_at": { + "name": "last_resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_incident_id": { + "name": "last_incident_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "anomaly_detector_states_org_idx": { + "name": "anomaly_detector_states_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "anomaly_detector_states_open_incident_idx": { + "name": "anomaly_detector_states_open_incident_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "open_incident_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "anomaly_detector_states_evaluated_idx": { + "name": "anomaly_detector_states_evaluated_idx", + "columns": [ + { + "expression": "last_evaluated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "anomaly_detector_states_org_id_detector_key_pk": { + "name": "anomaly_detector_states_org_id_detector_key_pk", + "columns": [ + "org_id", + "detector_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.anomaly_incidents": { + "name": "anomaly_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detector_key": { + "name": "detector_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signal_type": { + "name": "signal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_env": { + "name": "deployment_env", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "fingerprint_hash": { + "name": "fingerprint_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_issue_id": { + "name": "error_issue_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "opened_value": { + "name": "opened_value", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "baseline_median": { + "name": "baseline_median", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "baseline_sigma": { + "name": "baseline_sigma", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "threshold_value": { + "name": "threshold_value", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "last_observed_value": { + "name": "last_observed_value", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "last_sample_count": { + "name": "last_sample_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_triggered_at": { + "name": "first_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolve_reason": { + "name": "resolve_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "triage_status": { + "name": "triage_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprints_json": { + "name": "fingerprints_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "reopen_count": { + "name": "reopen_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_reopened_at": { + "name": "last_reopened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "anomaly_incidents_org_status_idx": { + "name": "anomaly_incidents_org_status_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "anomaly_incidents_org_triggered_idx": { + "name": "anomaly_incidents_org_triggered_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_triggered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "anomaly_incidents_org_detector_idx": { + "name": "anomaly_incidents_org_detector_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "detector_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "anomaly_incidents_org_issue_idx": { + "name": "anomaly_incidents_org_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "error_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'standard'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_email": { + "name": "created_by_email", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_keys_org_id_idx": { + "name": "api_keys_org_id_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloudflare_analytics_state": { + "name": "cloudflare_analytics_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dataset": { + "name": "dataset", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "zone_id": { + "name": "zone_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "zone_name": { + "name": "zone_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "watermark_at": { + "name": "watermark_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "backfill_at": { + "name": "backfill_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "settings_json": { + "name": "settings_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settings_fetched_at": { + "name": "settings_fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "quantiles_available": { + "name": "quantiles_available", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "discovered_at": { + "name": "discovered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "live_scripts_json": { + "name": "live_scripts_json", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "cf_analytics_state_org_dataset_zone_idx": { + "name": "cf_analytics_state_org_dataset_zone_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dataset", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "zone_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cf_analytics_state_org_idx": { + "name": "cf_analytics_state_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cloudflare_logpush_connectors": { + "name": "cloudflare_logpush_connectors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "zone_name": { + "name": "zone_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dataset": { + "name": "dataset", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'http_requests'" + }, + "secret_ciphertext": { + "name": "secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_iv": { + "name": "secret_iv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_tag": { + "name": "secret_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_hash": { + "name": "secret_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_received_at": { + "name": "last_received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_rotated_at": { + "name": "secret_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "cloudflare_logpush_connectors_org_idx": { + "name": "cloudflare_logpush_connectors_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cloudflare_logpush_connectors_org_enabled_idx": { + "name": "cloudflare_logpush_connectors_org_enabled_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cloudflare_logpush_connectors_secret_hash_unique": { + "name": "cloudflare_logpush_connectors_secret_hash_unique", + "columns": [ + { + "expression": "secret_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard_versions": { + "name": "dashboard_versions", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dashboard_id": { + "name": "dashboard_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_number": { + "name": "version_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "snapshot_json": { + "name": "snapshot_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "change_kind": { + "name": "change_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_version_id": { + "name": "source_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "dashboard_versions_org_dashboard_idx": { + "name": "dashboard_versions_org_dashboard_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dashboard_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dashboard_versions_org_dashboard_version_unq": { + "name": "dashboard_versions_org_dashboard_version_unq", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dashboard_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dashboard_versions_org_id_id_pk": { + "name": "dashboard_versions_org_id_id_pk", + "columns": [ + "org_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboards": { + "name": "dashboards", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "dashboards_org_updated_idx": { + "name": "dashboards_org_updated_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dashboards_org_name_idx": { + "name": "dashboards_org_name_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "dashboards_org_id_id_pk": { + "name": "dashboards_org_id_id_pk", + "columns": [ + "org_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.digest_subscriptions": { + "name": "digest_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "day_of_week": { + "name": "day_of_week", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "last_sent_at": { + "name": "last_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "digest_subscriptions_org_user_idx": { + "name": "digest_subscriptions_org_user_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "digest_subscriptions_org_enabled_idx": { + "name": "digest_subscriptions_org_enabled_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.actors": { + "name": "actors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities_json": { + "name": "capabilities_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "actors_org_user_idx": { + "name": "actors_org_user_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "actors_org_agent_name_idx": { + "name": "actors_org_agent_name_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "actors_org_type_idx": { + "name": "actors_org_type_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_incidents": { + "name": "error_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_triggered_at": { + "name": "first_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "occurrence_count": { + "name": "occurrence_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "error_incidents_org_issue_idx": { + "name": "error_incidents_org_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_incidents_org_status_idx": { + "name": "error_incidents_org_status_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_issue_events": { + "name": "error_issue_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_state": { + "name": "from_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to_state": { + "name": "to_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "error_issue_events_issue_idx": { + "name": "error_issue_events_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issue_events_actor_idx": { + "name": "error_issue_events_actor_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issue_events_type_idx": { + "name": "error_issue_events_type_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_issue_states": { + "name": "error_issue_states", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_observed_occurrence_at": { + "name": "last_observed_occurrence_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "open_incident_id": { + "name": "open_incident_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "error_issue_states_org_idx": { + "name": "error_issue_states_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "error_issue_states_org_id_issue_id_pk": { + "name": "error_issue_states_org_id_issue_id_pk", + "columns": [ + "org_id", + "issue_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_issues": { + "name": "error_issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'error'" + }, + "source_ref_json": { + "name": "source_ref_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "fingerprint_hash": { + "name": "fingerprint_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "exception_type": { + "name": "exception_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "exception_message": { + "name": "exception_message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_label": { + "name": "error_label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "top_frame": { + "name": "top_frame", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_state": { + "name": "workflow_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'triage'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "severity_source": { + "name": "severity_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_actor_id": { + "name": "assigned_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_holder_actor_id": { + "name": "lease_holder_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "occurrence_count": { + "name": "occurrence_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_by_actor_id": { + "name": "resolved_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "snooze_until": { + "name": "snooze_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "error_issues_org_fp_idx": { + "name": "error_issues_org_fp_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issues_org_workflow_idx": { + "name": "error_issues_org_workflow_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issues_org_severity_idx": { + "name": "error_issues_org_severity_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "severity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issues_org_last_seen_idx": { + "name": "error_issues_org_last_seen_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issues_org_assignee_idx": { + "name": "error_issues_org_assignee_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assigned_actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "error_issues_lease_expiry_idx": { + "name": "error_issues_lease_expiry_idx", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.error_notification_policies": { + "name": "error_notification_policies", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "destination_ids_json": { + "name": "destination_ids_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "notify_on_first_seen": { + "name": "notify_on_first_seen", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_on_regression": { + "name": "notify_on_regression", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_on_resolve": { + "name": "notify_on_resolve", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notify_on_transition_in_review": { + "name": "notify_on_transition_in_review", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notify_on_transition_done": { + "name": "notify_on_transition_done", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notify_on_claim": { + "name": "notify_on_claim", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "min_occurrence_count": { + "name": "min_occurrence_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'warning'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_escalation_policies": { + "name": "issue_escalation_policies", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rules_json": { + "name": "rules_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_escalations": { + "name": "issue_escalations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_json": { + "name": "payload_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "issue_escalations_dedupe_idx": { + "name": "issue_escalations_dedupe_idx", + "columns": [ + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_escalations_due_idx": { + "name": "issue_escalations_due_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_escalations_org_issue_idx": { + "name": "issue_escalations_org_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.investigations": { + "name": "investigations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'investigating'" + }, + "seeded_by": { + "name": "seeded_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "subject_json": { + "name": "subject_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "incident_kind": { + "name": "incident_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "incident_id": { + "name": "incident_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report_json": { + "name": "report_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "diagnosed_at": { + "name": "diagnosed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "investigations_incident_idx": { + "name": "investigations_incident_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "incident_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "incident_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"investigations\".\"incident_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "investigations_org_created_idx": { + "name": "investigations_org_created_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "investigations_org_issue_idx": { + "name": "investigations_org_issue_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "investigations_org_status_idx": { + "name": "investigations_org_status_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_auth_states": { + "name": "oauth_auth_states", + "schema": "", + "columns": { + "state": { + "name": "state", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "initiated_by_user_id": { + "name": "initiated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "return_to": { + "name": "return_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_auth_states_expires_idx": { + "name": "oauth_auth_states_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_connections": { + "name": "oauth_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_user_id": { + "name": "external_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_user_email": { + "name": "external_user_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_account_name": { + "name": "external_account_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "access_token_ciphertext": { + "name": "access_token_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_iv": { + "name": "access_token_iv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_tag": { + "name": "access_token_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token_ciphertext": { + "name": "refresh_token_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token_iv": { + "name": "refresh_token_iv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token_tag": { + "name": "refresh_token_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_connections_org_provider_idx": { + "name": "oauth_connections_org_provider_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_connections_org_idx": { + "name": "oauth_connections_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_onboarding_state": { + "name": "org_onboarding_state", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "demo_data_requested": { + "name": "demo_data_requested", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "onboarding_completed_at": { + "name": "onboarding_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "checklist_dismissed_at": { + "name": "checklist_dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "first_data_received_at": { + "name": "first_data_received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "welcome_email_sent_at": { + "name": "welcome_email_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "connect_nudge_email_sent_at": { + "name": "connect_nudge_email_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stalled_email_sent_at": { + "name": "stalled_email_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "activation_email_sent_at": { + "name": "activation_email_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_ingest_attribute_mappings": { + "name": "org_ingest_attribute_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_context": { + "name": "source_context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_ingest_attribute_mappings_org_idx": { + "name": "org_ingest_attribute_mappings_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_recommendation_issues": { + "name": "org_recommendation_issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "recommendation_key": { + "name": "recommendation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonical_key": { + "name": "canonical_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "usage_count": { + "name": "usage_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "org_recommendation_issues_org_idx": { + "name": "org_recommendation_issues_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_recommendation_issues_org_key_idx": { + "name": "org_recommendation_issues_org_key_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recommendation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_ingest_keys": { + "name": "org_ingest_keys", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key_hash": { + "name": "public_key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key_ciphertext": { + "name": "private_key_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key_iv": { + "name": "private_key_iv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key_tag": { + "name": "private_key_tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key_hash": { + "name": "private_key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_rotated_at": { + "name": "public_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "private_rotated_at": { + "name": "private_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "org_ingest_keys_public_key_unique": { + "name": "org_ingest_keys_public_key_unique", + "columns": [ + { + "expression": "public_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_ingest_keys_public_key_hash_unique": { + "name": "org_ingest_keys_public_key_hash_unique", + "columns": [ + { + "expression": "public_key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_ingest_keys_private_key_hash_unique": { + "name": "org_ingest_keys_private_key_hash_unique", + "columns": [ + { + "expression": "private_key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "org_ingest_keys_org_id_pk": { + "name": "org_ingest_keys_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_ingest_sampling_policies": { + "name": "org_ingest_sampling_policies", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "trace_sample_ratio": { + "name": "trace_sample_ratio", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "always_keep_error_spans": { + "name": "always_keep_error_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "always_keep_slow_spans_ms": { + "name": "always_keep_slow_spans_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_clickhouse_settings": { + "name": "org_clickhouse_settings", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ch_url": { + "name": "ch_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ch_user": { + "name": "ch_user", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ch_password_ciphertext": { + "name": "ch_password_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ch_password_iv": { + "name": "ch_password_iv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ch_password_tag": { + "name": "ch_password_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ch_database": { + "name": "ch_database", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "org_clickhouse_settings_org_id_pk": { + "name": "org_clickhouse_settings_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_clickhouse_schema_apply_runs": { + "name": "org_clickhouse_schema_apply_runs", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_instance_id": { + "name": "workflow_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_migration": { + "name": "current_migration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "steps_total": { + "name": "steps_total", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "steps_done": { + "name": "steps_done", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "applied_versions": { + "name": "applied_versions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "skipped": { + "name": "skipped", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "org_clickhouse_schema_apply_runs_org_id_pk": { + "name": "org_clickhouse_schema_apply_runs_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scrape_target_checks": { + "name": "scrape_target_checks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "byDefault", + "name": "scrape_target_checks_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_target_key": { + "name": "sub_target_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "samples_scraped": { + "name": "samples_scraped", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "samples_post_relabel": { + "name": "samples_post_relabel", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scrape_target_checks_target_checked_idx": { + "name": "scrape_target_checks_target_checked_idx", + "columns": [ + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "checked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scrape_target_checks_target_id_scrape_targets_id_fk": { + "name": "scrape_target_checks_target_id_scrape_targets_id_fk", + "tableFrom": "scrape_target_checks", + "tableTo": "scrape_targets", + "columnsFrom": [ + "target_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scrape_targets": { + "name": "scrape_targets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'prometheus'" + }, + "discovery_config_json": { + "name": "discovery_config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scrape_interval_seconds": { + "name": "scrape_interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "labels_json": { + "name": "labels_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "auth_credentials_ciphertext": { + "name": "auth_credentials_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_credentials_iv": { + "name": "auth_credentials_iv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_credentials_tag": { + "name": "auth_credentials_tag", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_scrape_at": { + "name": "last_scrape_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_scrape_error": { + "name": "last_scrape_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scrape_targets_org_idx": { + "name": "scrape_targets_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scrape_targets_org_enabled_idx": { + "name": "scrape_targets_org_enabled_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vcs_commits": { + "name": "vcs_commits", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sha": { + "name": "sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_email": { + "name": "author_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_login": { + "name": "author_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_avatar_url": { + "name": "author_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authored_at": { + "name": "authored_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "committed_at": { + "name": "committed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "html_url": { + "name": "html_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vcs_commits_repo_sha_idx": { + "name": "vcs_commits_repo_sha_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vcs_commits_org_sha_idx": { + "name": "vcs_commits_org_sha_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vcs_installations": { + "name": "vcs_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_installation_id": { + "name": "external_installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_account_id": { + "name": "external_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_avatar_url": { + "name": "account_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_selection": { + "name": "repository_selection", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "installed_by_user_id": { + "name": "installed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vcs_installations_provider_external_idx": { + "name": "vcs_installations_provider_external_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vcs_installations_org_idx": { + "name": "vcs_installations_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vcs_repositories": { + "name": "vcs_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_repo_id": { + "name": "external_repo_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "tracked_branch": { + "name": "tracked_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "html_url": { + "name": "html_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_private": { + "name": "is_private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vcs_repositories_org_repo_idx": { + "name": "vcs_repositories_org_repo_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_repo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vcs_repositories_org_idx": { + "name": "vcs_repositories_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vcs_repositories_installation_idx": { + "name": "vcs_repositories_installation_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vcs_repository_branches": { + "name": "vcs_repository_branches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "vcs_repository_branches_repo_name_idx": { + "name": "vcs_repository_branches_repo_name_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vcs_repository_branches_org_idx": { + "name": "vcs_repository_branches_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 269df9552..0fae9f28b 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -1,83 +1,90 @@ { - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1781306960498, - "tag": "0000_windy_raza", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1782245542758, - "tag": "0001_naive_scalphunter", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1782656894999, - "tag": "0002_bizarre_triathlon", - "breakpoints": true - }, - { - "idx": 3, - "version": "7", - "when": 1782992101248, - "tag": "0003_backfill_github_commit_avatars", - "breakpoints": true - }, - { - "idx": 4, - "version": "7", - "when": 1783200000000, - "tag": "0004_premium_korg", - "breakpoints": true - }, - { - "idx": 5, - "version": "7", - "when": 1783200001000, - "tag": "0005_loud_pyro", - "breakpoints": true - }, - { - "idx": 6, - "version": "7", - "when": 1783200002000, - "tag": "0006_natural_marvel_apes", - "breakpoints": true - }, - { - "idx": 7, - "version": "7", - "when": 1783200003000, - "tag": "0007_slippery_winter_soldier", - "breakpoints": true - }, - { - "idx": 8, - "version": "7", - "when": 1783200004000, - "tag": "0008_elite_butterfly", - "breakpoints": true - }, - { - "idx": 9, - "version": "7", - "when": 1783200005000, - "tag": "0009_electric_publication", - "breakpoints": true - }, - { - "idx": 10, - "version": "7", - "when": 1783377967610, - "tag": "0010_huge_dexter_bennett", - "breakpoints": true - } - ] -} \ No newline at end of file + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1781306960498, + "tag": "0000_windy_raza", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1782245542758, + "tag": "0001_naive_scalphunter", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1782656894999, + "tag": "0002_bizarre_triathlon", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1782992101248, + "tag": "0003_backfill_github_commit_avatars", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1783200000000, + "tag": "0004_premium_korg", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1783200001000, + "tag": "0005_loud_pyro", + "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1783200002000, + "tag": "0006_natural_marvel_apes", + "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1783200003000, + "tag": "0007_slippery_winter_soldier", + "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1783200004000, + "tag": "0008_elite_butterfly", + "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1783200005000, + "tag": "0009_electric_publication", + "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1783377967610, + "tag": "0010_huge_dexter_bennett", + "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1783377967700, + "tag": "0011_electric_publication_wave1", + "breakpoints": true + } + ] +} diff --git a/packages/db/src/migrations.test.ts b/packages/db/src/migrations.test.ts index 9be29cc91..742e6769a 100644 --- a/packages/db/src/migrations.test.ts +++ b/packages/db/src/migrations.test.ts @@ -56,6 +56,9 @@ describe("bundled migrations", () => { "error_issues", "actors", "error_incidents", + // Wave 1 (0011_electric_publication_wave1) + "alert_destinations", + "scrape_target_checks", ] it( From e5a82ece6db05ad2e42c822fcac9f4be8350d457 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Wed, 8 Jul 2026 18:26:44 +0200 Subject: [PATCH 2/4] w --- apps/web/src/components/dashboard-builder/types.ts | 3 ++- apps/web/src/hooks/use-scrape-target-checks.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/dashboard-builder/types.ts b/apps/web/src/components/dashboard-builder/types.ts index 94112b05a..b9e62c8f8 100644 --- a/apps/web/src/components/dashboard-builder/types.ts +++ b/apps/web/src/components/dashboard-builder/types.ts @@ -8,6 +8,7 @@ // --------------------------------------------------------------------------- import type { + DashboardId, DashboardVariableSchema, DashboardWidgetSchema, WidgetDataSourceSchema, @@ -122,7 +123,7 @@ export type DashboardVariable = DeepMutable // --- Dashboard --- export interface Dashboard { - id: string + id: DashboardId name: string description?: string tags?: string[] diff --git a/apps/web/src/hooks/use-scrape-target-checks.ts b/apps/web/src/hooks/use-scrape-target-checks.ts index b2c7ffeab..e1ee80b7d 100644 --- a/apps/web/src/hooks/use-scrape-target-checks.ts +++ b/apps/web/src/hooks/use-scrape-target-checks.ts @@ -1,4 +1,4 @@ -import { ScrapeTargetChecksListResponse } from "@maple/domain/http" +import { ScrapeTargetChecksListResponse, type ScrapeTargetId } from "@maple/domain/http" import { useLiveQuery } from "@tanstack/react-db" import { useMemo } from "react" import { rowToScrapeTargetCheckDocument } from "@/lib/collections/scrape-targets" @@ -26,7 +26,7 @@ const noop = () => {} * the target client-side (bounded to 24h / 10k-per-target by server pruning), * exactly like `useAlertRuleStates` filters by rule id. */ -export function useScrapeTargetChecks(targetId: string, limit: number): ScrapeTargetChecksHook { +export function useScrapeTargetChecks(targetId: ScrapeTargetId, limit: number): ScrapeTargetChecksHook { const orgKey = useActiveOrgId() ?? "pending" const generation = useCollectionsGeneration() const collection = useMemo( From 190518c25dd4c1f23114f4546af09b85a09712a9 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Wed, 8 Jul 2026 18:33:06 +0200 Subject: [PATCH 3/4] a --- apps/web/src/atoms/dashboard-history-atoms.ts | 3 ++- .../dashboard-actions-context.tsx | 5 +++-- .../history/dashboard-history-panel.tsx | 5 +++-- .../history/preview-banner.tsx | 3 ++- .../history/previewed-canvas.tsx | 3 ++- .../history/use-dashboard-history.ts | 22 ++++++++----------- .../src/components/dashboard-builder/types.ts | 3 +-- apps/web/src/hooks/use-alert-rule-states.ts | 3 ++- apps/web/src/routes/alerts/$ruleId.tsx | 10 +++++++-- .../src/routes/dashboards/$dashboardId.tsx | 13 ++++++++--- 10 files changed, 42 insertions(+), 28 deletions(-) diff --git a/apps/web/src/atoms/dashboard-history-atoms.ts b/apps/web/src/atoms/dashboard-history-atoms.ts index df731ff7e..5b918bfdd 100644 --- a/apps/web/src/atoms/dashboard-history-atoms.ts +++ b/apps/web/src/atoms/dashboard-history-atoms.ts @@ -1,7 +1,8 @@ import { Atom } from "@/lib/effect-atom" +import type { DashboardVersionId } from "@maple/domain/http" export interface PreviewedVersion { - readonly versionId: string + readonly versionId: DashboardVersionId readonly versionNumber: number readonly createdAt: string readonly createdBy: string diff --git a/apps/web/src/components/dashboard-builder/dashboard-actions-context.tsx b/apps/web/src/components/dashboard-builder/dashboard-actions-context.tsx index 47e17ad0b..3dd463f30 100644 --- a/apps/web/src/components/dashboard-builder/dashboard-actions-context.tsx +++ b/apps/web/src/components/dashboard-builder/dashboard-actions-context.tsx @@ -1,5 +1,6 @@ import * as React from "react" import type { ReactNode } from "react" +import type { DashboardId } from "@maple/domain/http" import { useNavigate } from "@tanstack/react-router" import { toast } from "sonner" import { pickVariableParams } from "@/lib/dashboard-variables/search-params" @@ -13,7 +14,7 @@ import type { } from "@/components/dashboard-builder/types" interface DashboardActionsContextValue { - dashboardId: string + dashboardId: DashboardId mode: WidgetMode readOnly: boolean removeWidget: (widgetId: string) => void @@ -34,7 +35,7 @@ const DashboardActionsContext = React.createContext void + onPreview: (versionId: DashboardVersionId) => void onClose: () => void } diff --git a/apps/web/src/components/dashboard-builder/history/preview-banner.tsx b/apps/web/src/components/dashboard-builder/history/preview-banner.tsx index f70dd6c63..e89658fa5 100644 --- a/apps/web/src/components/dashboard-builder/history/preview-banner.tsx +++ b/apps/web/src/components/dashboard-builder/history/preview-banner.tsx @@ -11,13 +11,14 @@ import { DialogHeader, DialogTitle, } from "@maple/ui/components/ui/dialog" +import type { DashboardId } from "@maple/domain/http" import { ArrowPathIcon, HistoryIcon } from "@/components/icons" import { formatRelativeTime } from "@/lib/format" import { buildRestorePayload, useRestoreDashboardVersion } from "./use-dashboard-history" import type { PreviewedVersion } from "@/atoms/dashboard-history-atoms" interface PreviewBannerProps { - dashboardId: string + dashboardId: DashboardId preview: PreviewedVersion onCancel: () => void onRestored: () => void diff --git a/apps/web/src/components/dashboard-builder/history/previewed-canvas.tsx b/apps/web/src/components/dashboard-builder/history/previewed-canvas.tsx index ad7592831..24c0f595f 100644 --- a/apps/web/src/components/dashboard-builder/history/previewed-canvas.tsx +++ b/apps/web/src/components/dashboard-builder/history/previewed-canvas.tsx @@ -1,3 +1,4 @@ +import type { DashboardId } from "@maple/domain/http" import { Result } from "@/lib/effect-atom" import type { DashboardWidget } from "@/components/dashboard-builder/types" import { DashboardCanvas } from "@/components/dashboard-builder/canvas/dashboard-canvas" @@ -6,7 +7,7 @@ import { useDashboardVersionDetail } from "./use-dashboard-history" import type { PreviewedVersion } from "@/atoms/dashboard-history-atoms" interface PreviewedCanvasProps { - dashboardId: string + dashboardId: DashboardId preview: PreviewedVersion onCancel: () => void onRestored: () => void diff --git a/apps/web/src/components/dashboard-builder/history/use-dashboard-history.ts b/apps/web/src/components/dashboard-builder/history/use-dashboard-history.ts index bad100301..ffdd6d295 100644 --- a/apps/web/src/components/dashboard-builder/history/use-dashboard-history.ts +++ b/apps/web/src/components/dashboard-builder/history/use-dashboard-history.ts @@ -1,16 +1,12 @@ -import { Schema } from "effect" import { useAtomSet, useAtomValue } from "@/lib/effect-atom" import { MapleApiAtomClient } from "@/lib/services/common/atom-client" import { DashboardId, DashboardVersionId } from "@maple/domain/http" -const asDashboardId = Schema.decodeUnknownSync(DashboardId) -const asDashboardVersionId = Schema.decodeUnknownSync(DashboardVersionId) +const dashboardVersionsKey = (dashboardId: DashboardId) => `dashboard:${dashboardId}:versions` -const dashboardVersionsKey = (dashboardId: string) => `dashboard:${dashboardId}:versions` - -export function useDashboardVersions(dashboardId: string) { +export function useDashboardVersions(dashboardId: DashboardId) { const queryAtom = MapleApiAtomClient.query("dashboards", "listVersions", { - params: { dashboardId: asDashboardId(dashboardId) }, + params: { dashboardId }, query: { limit: 100 }, reactivityKeys: [dashboardVersionsKey(dashboardId)], }) @@ -21,11 +17,11 @@ export function useDashboardVersions(dashboardId: string) { * Fetch a single version's full snapshot. The parent must conditionally * mount the consuming component — this hook is always called on mount. */ -export function useDashboardVersionDetail(dashboardId: string, versionId: string) { +export function useDashboardVersionDetail(dashboardId: DashboardId, versionId: DashboardVersionId) { const queryAtom = MapleApiAtomClient.query("dashboards", "getVersion", { params: { - dashboardId: asDashboardId(dashboardId), - versionId: asDashboardVersionId(versionId), + dashboardId, + versionId, }, reactivityKeys: [`dashboard:${dashboardId}:version:${versionId}`], }) @@ -36,10 +32,10 @@ export function useRestoreDashboardVersion() { return useAtomSet(MapleApiAtomClient.mutation("dashboards", "restoreVersion"), { mode: "promiseExit" }) } -export const buildRestorePayload = (dashboardId: string, versionId: string) => ({ +export const buildRestorePayload = (dashboardId: DashboardId, versionId: DashboardVersionId) => ({ params: { - dashboardId: asDashboardId(dashboardId), - versionId: asDashboardVersionId(versionId), + dashboardId, + versionId, }, reactivityKeys: ["dashboards", dashboardVersionsKey(dashboardId)] as const, }) diff --git a/apps/web/src/components/dashboard-builder/types.ts b/apps/web/src/components/dashboard-builder/types.ts index b9e62c8f8..94112b05a 100644 --- a/apps/web/src/components/dashboard-builder/types.ts +++ b/apps/web/src/components/dashboard-builder/types.ts @@ -8,7 +8,6 @@ // --------------------------------------------------------------------------- import type { - DashboardId, DashboardVariableSchema, DashboardWidgetSchema, WidgetDataSourceSchema, @@ -123,7 +122,7 @@ export type DashboardVariable = DeepMutable // --- Dashboard --- export interface Dashboard { - id: DashboardId + id: string name: string description?: string tags?: string[] diff --git a/apps/web/src/hooks/use-alert-rule-states.ts b/apps/web/src/hooks/use-alert-rule-states.ts index e1102a8e7..a93266700 100644 --- a/apps/web/src/hooks/use-alert-rule-states.ts +++ b/apps/web/src/hooks/use-alert-rule-states.ts @@ -1,5 +1,6 @@ import { useLiveQuery } from "@tanstack/react-db" import { useMemo } from "react" +import type { AlertRuleId } from "@maple/domain/http" import type { AlertRuleStateRow } from "@/lib/collections/alerts" import { getOrgCollections, @@ -12,7 +13,7 @@ import { * sample count/evaluated-at/error per `(ruleId, groupKey)`), synced via * Electric. Powers the overview status derivation and the rule diagnosis panel. */ -export function useAlertRuleStates(ruleId?: string): ReadonlyArray { +export function useAlertRuleStates(ruleId?: AlertRuleId): ReadonlyArray { const orgKey = useActiveOrgId() ?? "pending" const generation = useCollectionsGeneration() const collection = useMemo( diff --git a/apps/web/src/routes/alerts/$ruleId.tsx b/apps/web/src/routes/alerts/$ruleId.tsx index 1cad28830..25318115c 100644 --- a/apps/web/src/routes/alerts/$ruleId.tsx +++ b/apps/web/src/routes/alerts/$ruleId.tsx @@ -78,6 +78,11 @@ import { formatSql } from "@/lib/sql-format" const tabValues = ["overview", "history"] as const type RuleDetailTab = (typeof tabValues)[number] +// Decode the raw `$ruleId` URL segment into its branded id once, at the route +// boundary, so the branded value threads through the checks/states queries +// without a per-call cast. +const asAlertRuleId = Schema.decodeSync(AlertRuleId) + const RuleDetailSearch = Schema.Struct({ tab: Schema.optional(Schema.String), startTime: Schema.optional(Schema.String), @@ -100,7 +105,8 @@ function RuleDetailPage() { } function RuleDetailContent() { - const { ruleId } = Route.useParams() + const { ruleId: ruleIdParam } = Route.useParams() + const ruleId = asAlertRuleId(ruleIdParam) const search = Route.useSearch() const navigate = useNavigate({ from: Route.fullPath }) @@ -135,7 +141,7 @@ function RuleDetailContent() { mode: "promiseExit", }) const checksQueryAtom = MapleApiAtomClient.query("alerts", "listRuleChecks", { - params: { ruleId: ruleId as AlertRuleId }, + params: { ruleId }, query: { since, until }, reactivityKeys: ["alertChecks", ruleId, since, until], }) diff --git a/apps/web/src/routes/dashboards/$dashboardId.tsx b/apps/web/src/routes/dashboards/$dashboardId.tsx index dfd6d3960..f33429e37 100644 --- a/apps/web/src/routes/dashboards/$dashboardId.tsx +++ b/apps/web/src/routes/dashboards/$dashboardId.tsx @@ -1,6 +1,7 @@ import { createFileRoute, Link, useNavigate } from "@tanstack/react-router" import { effectRoute } from "@effect-router/core" import { Schema } from "effect" +import { DashboardId, DashboardVersionId } from "@maple/domain/http" import { Atom, useAtom } from "@/lib/effect-atom" import { DashboardLayout } from "@/components/layout/dashboard-layout" @@ -34,6 +35,11 @@ import { useMemo, type ReactNode } from "react" // Module-level atoms — singleton (only one dashboard page visible at a time) const chartPickerOpenAtom = Atom.make(false) +// Decode the raw `$dashboardId` URL segment into its branded id once, at the +// route boundary, so the branded value threads through the store/history hooks +// without a per-call cast. +const asDashboardId = Schema.decodeSync(DashboardId) + // `var-` keys carry dashboard-variable selections (Grafana-style), so // views are shareable/deep-linkable. Values are `Unknown` on purpose: TanStack // JSON-parses each search value, and a hand-edited URL must never crash the @@ -72,7 +78,8 @@ function DashboardRefreshBridge({ children }: { children: ReactNode }) { } function DashboardViewPage() { - const { dashboardId } = Route.useParams() + const { dashboardId: dashboardIdParam } = Route.useParams() + const dashboardId = asDashboardId(dashboardIdParam) const search = Route.useSearch() const navigate = useNavigate() @@ -287,11 +294,11 @@ function DashboardViewPage() { ) } -function HistoryPanelMount({ dashboardId, onClose }: { dashboardId: string; onClose: () => void }) { +function HistoryPanelMount({ dashboardId, onClose }: { dashboardId: DashboardId; onClose: () => void }) { const [previewed, setPreviewed] = useAtom(previewedVersionAtom) const result = useDashboardVersions(dashboardId) - const onPreview = (versionId: string) => { + const onPreview = (versionId: DashboardVersionId) => { if (!Result.isSuccess(result)) return const version = result.value.versions.find((v) => v.id === versionId) if (!version) return From 5e213169f9484d29e14b17591ea060a6e7429d09 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Wed, 8 Jul 2026 18:34:24 +0200 Subject: [PATCH 4/4] fix(alerts): drop unreachable no-op destinations Retry in escalation policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useAlertDestinationsList() resolves off the live-synced collection, so its Result is only ever `initial` (loading) or `success` — never a failure. The `.onError` branch in the escalation policy section was therefore unreachable dead code, and its "Retry" button was wired to the hook's no-op refresh. Remove the dead error/retry branch; loading shows the skeleton and success renders the form. Addresses Devin review finding on escalation-policy-section.tsx. Co-Authored-By: Claude Opus 4.8 --- .../settings/escalation-policy-section.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/web/src/components/settings/escalation-policy-section.tsx b/apps/web/src/components/settings/escalation-policy-section.tsx index c9aec6f01..6ff3c4b34 100644 --- a/apps/web/src/components/settings/escalation-policy-section.tsx +++ b/apps/web/src/components/settings/escalation-policy-section.tsx @@ -60,7 +60,7 @@ export function EscalationPolicySection({ isAdmin }: { isAdmin: boolean }) { const policyResult = useAtomValue(policyQueryAtom) const refreshPolicy = useAtomRefresh(policyQueryAtom) - const { result: destinationsResult, refresh: refreshDestinations } = useAlertDestinationsList() + const { result: destinationsResult } = useAlertDestinationsList() const upsertMutation = useAtomSet(MapleApiAtomClient.mutation("errors", "upsertEscalationPolicy"), { mode: "promiseExit", @@ -150,15 +150,10 @@ export function EscalationPolicySection({ isAdmin }: { isAdmin: boolean }) { {Result.builder(destinationsResult) + // Destinations come from the live-synced collection, which only + // resolves to `initial` (loading) or `success` — never a failure — + // so there is no error/retry branch to render here. .onInitial(() => ) - .onError(() => ( -
- Failed to load alert destinations. - -
- )) .onSuccess((response) => { if (response.destinations.length === 0) { return (