/**
* Uptime monitoring schema — OWNED BY THE UPTIME MONITORING FEATURE.
*
* Everything exported here is re-exported from ./schema.ts via `export *`, so
* these tables join the app's Drizzle schema namespace and are reachable as
* `schema.
` throughout the server. Keep all schema changes additive
* (never drop/rename/retype existing columns) per the storing-data rules.
*
* Physical table creation + indexes are applied by the isolated migration list
* in server/plugins/uptime-monitor-jobs.ts (`uptime_monitor_migrations`), NOT
* by drizzle-kit. These Drizzle definitions must stay in lockstep with that
* DDL (snake_case column names, dialect-agnostic types).
*
* All three tables carry `ownableColumns()` (owner_email / org_id / visibility)
* and every read/write in server/lib/uptime-monitors.ts is scoped by
* owner_email + org_id, mirroring the analytics-alerts engine.
*/
import {
integer,
now,
ownableColumns,
table,
text,
} from "@agent-native/core/db/schema";
/**
* A single uptime check ("monitor"). Pings `url` on `intervalSeconds` and
* evaluates the response against a status matcher + a list of assertions.
*/
export const monitors = table("monitors", {
id: text("id").primaryKey(),
name: text("name").notNull(),
url: text("url").notNull(),
/** HTTP method used for the probe. */
method: text("method", {
enum: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
})
.notNull()
.default("GET"),
/** Extra request headers as a JSON object (`Record`). */
requestHeaders: text("request_headers").notNull().default("{}"),
/** Optional request body (POST/PUT/PATCH). */
requestBody: text("request_body"),
intervalSeconds: integer("interval_seconds").notNull().default(300),
timeoutMs: integer("timeout_ms").notNull().default(10000),
/**
* Status matcher as JSON. One of:
* { "mode": "class", "classes": ["2xx","3xx"] }
* { "mode": "list", "codes": [200,204] }
* { "mode": "range", "min": 200, "max": 299 }
*/
expectedStatus: text("expected_status")
.notNull()
.default('{"mode":"class","classes":["2xx"]}'),
/**
* JSON array of assertions. Each entry:
* { "type": "body_contains" | "body_absent" | "header_contains" |
* "header_equals" | "max_latency_ms",
* "value": string | number, "header"?: string }
*/
assertions: text("assertions").notNull().default("[]"),
followRedirects: integer("follow_redirects", { mode: "boolean" })
.notNull()
.default(true),
/** Alert severity when the monitor fails. */
severity: text("severity", { enum: ["warning", "critical"] })
.notNull()
.default("critical"),
/** Notification channels as JSON (e.g. ["inbox","email","slack","webhook"]). */
channels: text("channels").notNull().default('["inbox"]'),
/** Email recipients as JSON array of addresses. */
emailRecipients: text("email_recipients").notNull().default("[]"),
/** Optional per-monitor Slack incoming webhook URL (overrides workspace env). */
slackWebhookUrl: text("slack_webhook_url"),
/** Optional per-monitor generic webhook URL (overrides workspace env). */
webhookUrl: text("webhook_url"),
/** Minutes to suppress repeat alerts while an incident is ongoing. */
cooldownMinutes: integer("cooldown_minutes").notNull().default(15),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
// ---- Mutable status fields (updated by the runner) ----
/** up | down | degraded | error | unknown | running */
lastStatus: text("last_status"),
lastCheckedAt: text("last_checked_at"),
lastSuccessAt: text("last_success_at"),
lastError: text("last_error"),
lastLatencyMs: integer("last_latency_ms"),
lastStatusCode: integer("last_status_code"),
consecutiveFailures: integer("consecutive_failures").notNull().default(0),
createdAt: text("created_at").notNull().default(now()),
updatedAt: text("updated_at").notNull().default(now()),
...ownableColumns(),
});
/**
* Historical probe results. Bounded retention is enforced by the sweep job
* (server/jobs/uptime-monitors.ts) so this table cannot grow unbounded.
*/
export const monitorCheckResults = table("monitor_check_results", {
id: text("id").primaryKey(),
monitorId: text("monitor_id").notNull(),
checkedAt: text("checked_at").notNull(),
ok: integer("ok", { mode: "boolean" }).notNull(),
/** Classification for this probe: up | down | degraded | error. */
status: text("status").notNull().default("up"),
statusCode: integer("status_code"),
latencyMs: integer("latency_ms"),
error: text("error"),
/** JSON array of human-readable assertion failures. */
failedAssertions: text("failed_assertions").notNull().default("[]"),
/** Compact JSON with phase timings and safe runtime/response metadata. */
diagnostics: text("diagnostics").notNull().default("{}"),
createdAt: text("created_at").notNull().default(now()),
...ownableColumns(),
});
/**
* One incident per continuous failure streak. `resolvedAt` null = ongoing.
* Mirrors analyticsAlertIncidents.
*/
export const monitorIncidents = table("monitor_incidents", {
id: text("id").primaryKey(),
monitorId: text("monitor_id").notNull(),
startedAt: text("started_at").notNull(),
resolvedAt: text("resolved_at"),
/** down | degraded — the classification at incident open. */
status: text("status").notNull().default("down"),
severity: text("severity").notNull().default("critical"),
/** Short summary of what failed (status code / failed assertions). */
cause: text("cause").notNull().default(""),
lastError: text("last_error"),
notificationId: text("notification_id"),
notificationDelivered: integer("notification_delivered", { mode: "boolean" })
.notNull()
.default(false),
checksFailed: integer("checks_failed").notNull().default(1),
createdAt: text("created_at").notNull().default(now()),
...ownableColumns(),
});
/**
* Owner-authored public status page. A status page bundles a set of the owner's
* monitors under a public `slug` (`/status/`) and renders their SAFE
* aggregate health (status, uptime %s, colored timelines) to anyone with the
* link — but only when `published` is true. The public read
* (server/lib/status-pages.ts `getPublicStatusPage`) strictly filters to
* published pages and the page owner's included monitors, and never leaks
* monitor URLs/headers/assertions/alert config unless a per-monitor "show URL"
* opt-in is set.
*
* `monitors` is a JSON array of
* { monitorId: string; order: number; displayName?: string | null;
* showUrl?: boolean }
* kept additive so page layout can grow without a join table migration.
*/
export const statusPages = table("status_pages", {
id: text("id").primaryKey(),
/** Public, globally-unique URL slug (`/status/`). */
slug: text("slug").notNull(),
title: text("title").notNull(),
description: text("description"),
/** Only published pages are readable by the unauthenticated public route. */
published: integer("published", { mode: "boolean" }).notNull().default(false),
// ---- Layout options ----
showUptimeBars: integer("show_uptime_bars", { mode: "boolean" })
.notNull()
.default(true),
showOverallUptime: integer("show_overall_uptime", { mode: "boolean" })
.notNull()
.default(true),
showResponseTime: integer("show_response_time", { mode: "boolean" })
.notNull()
.default(false),
density: text("density", { enum: ["comfortable", "compact"] })
.notNull()
.default("comfortable"),
alignment: text("alignment", { enum: ["left", "center"] })
.notNull()
.default("left"),
/** JSON array of included monitors with order + optional display overrides. */
monitors: text("monitors").notNull().default("[]"),
createdAt: text("created_at").notNull().default(now()),
updatedAt: text("updated_at").notNull().default(now()),
...ownableColumns(),
});