diff --git a/CONTEXT.md b/CONTEXT.md index 5c34b7ca..cca68d89 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -46,7 +46,7 @@ A Monitoring Sample produced by a check that actually ran against the target, wh A Monitoring Sample written by the system or an admin rather than by a check: a raw heartbeat receipt (`SIGNAL`), a status pushed through the data API (`MANUAL`), a default-status fill (`DEFAULT_STATUS`), or an incident/maintenance overlay (`INCIDENT`, `MAINTENANCE`). **Stale Member**: -A Member whose monitor is no longer an Eligible Monitor (paused or deleted after being added). It remains a Member until explicitly removed, but is excluded from the group score. +A Member whose monitor was paused after being added, making it no longer an Eligible Monitor. It remains a Member until explicitly removed, but is excluded from the group score. Deletion never produces a Stale Member: deleting a monitor strips it from every group and rebalances the remaining members' weights equally. ### Alerting diff --git a/docs/adr/0008-explicit-deletes-over-fk-cascades.md b/docs/adr/0008-explicit-deletes-over-fk-cascades.md new file mode 100644 index 00000000..372d43a2 --- /dev/null +++ b/docs/adr/0008-explicit-deletes-over-fk-cascades.md @@ -0,0 +1,9 @@ +# Delete paths remove child rows explicitly and never rely on FK cascades + +The schema declares `ON DELETE CASCADE` foreign keys (alert configs → monitors, trigger/monitor junctions → configs, v2 alerts → configs), but SQLite — the default deployment — never enforces them: knex does not enable the `foreign_keys` pragma, and `knexfile.ts` does not either. Code that trusted the declared cascades (`deleteMonitorAlertConfig`, and `DeleteMonitorCompletelyUsingTag`, which skipped alert configs entirely) silently orphaned junction rows, configs, and `monitor_alerts_v2` state on SQLite, while behaving correctly on Postgres and MySQL (#716). + +We considered fixing the root cause instead: enabling `PRAGMA foreign_keys = ON` per connection via `pool.afterCreate`. That would make every declared cascade real and align the three databases. We rejected it because knex implements SQLite `ALTER TABLE` as a table rebuild, which violates FK constraints transiently — the multi-monitor-alerts migration already has to toggle the pragma off around its own rebuild — so enforcement would put every past and future migration on a tightrope, and it changes write-failure behavior for all existing SQLite installs at once (inserts that used to succeed against missing parents would start throwing). + +The rule is the opposite and uniform: a delete path owns its children and removes them explicitly, in child-before-parent order. `deleteMonitorAlertConfig` deletes v2 alerts, trigger junctions, and monitor junctions before the config row; `deleteMonitorAlertConfigsByMonitorTag` detaches the monitor and routes zero-monitor configs through that same method; `DeleteMonitorCompletelyUsingTag` calls it alongside its other per-tag cleanups. The declared cascades stay in the schema — on Postgres/MySQL they make the explicit deletes idempotent no-ops, and they document intent — but no code may depend on them. + +A consequence: shared alert configs (one config spanning several monitors) survive the deletion of one member; a config dies only when its last monitor is detached. Monitor deletion also strips the tag from group monitors and rebalances remaining member weights equally — deletion never produces a stale member (see CONTEXT.md, "Stale Member"). diff --git a/src/lib/server/controllers/monitorAlertConfigController.ts b/src/lib/server/controllers/monitorAlertConfigController.ts index 2f118407..f91b6307 100644 --- a/src/lib/server/controllers/monitorAlertConfigController.ts +++ b/src/lib/server/controllers/monitorAlertConfigController.ts @@ -328,7 +328,8 @@ export async function DeleteMonitorAlertConfig(id: number): Promise { throw new Error(`Monitor alert config with id '${id}' not found`); } - // Triggers will be deleted automatically due to CASCADE + // The repository deletes trigger/monitor junctions and v2 alerts explicitly; + // FK cascades are not enforced on SQLite const deleted = await db.deleteMonitorAlertConfig(id); return deleted > 0; } diff --git a/src/lib/server/controllers/monitorsController.ts b/src/lib/server/controllers/monitorsController.ts index cb5d3ab6..53ba366d 100644 --- a/src/lib/server/controllers/monitorsController.ts +++ b/src/lib/server/controllers/monitorsController.ts @@ -437,6 +437,7 @@ export const DeleteMonitorCompletelyUsingTag = async (tag: string): Promise { + await this.knex("monitor_alerts_v2").where({ config_id: id }).del(); + await this.knex("monitor_alerts_config_triggers").where({ monitor_alerts_id: id }).del(); + await this.knex("monitor_alerts_config_monitors").where({ monitor_alerts_id: id }).del(); return await this.knex("monitor_alerts_config").where({ id }).del(); } @@ -170,7 +178,7 @@ export class MonitorAlertConfigRepository extends BaseRepository { .where({ monitor_alerts_id: id }) .first(); if (Number(remainingMonitors?.count) === 0) { - await this.knex("monitor_alerts_config").where({ id }).del(); + await this.deleteMonitorAlertConfig(id); deletedCount++; } } diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts index 2fd49a1c..791c39d7 100644 --- a/src/lib/types/api.ts +++ b/src/lib/types/api.ts @@ -144,6 +144,10 @@ export interface UpdateMonitorResponse { monitor: MonitorRecordTyped; } +export interface DeleteMonitorResponse { + message: string; +} + // Monitoring Data API types export interface MonitoringDataPoint { monitor_tag: string; diff --git a/src/routes/(api)/api/v4/monitors/[monitor_tag]/+server.ts b/src/routes/(api)/api/v4/monitors/[monitor_tag]/+server.ts index ee39c62e..7afe77ee 100644 --- a/src/routes/(api)/api/v4/monitors/[monitor_tag]/+server.ts +++ b/src/routes/(api)/api/v4/monitors/[monitor_tag]/+server.ts @@ -1,11 +1,12 @@ import { json, type RequestHandler } from "@sveltejs/kit"; import db from "$lib/server/db/db"; -import { GetMonitorsParsed } from "$lib/server/controllers/monitorsController"; +import { GetMonitorsParsed, DeleteMonitorCompletelyUsingTag } from "$lib/server/controllers/monitorsController"; import type { GetMonitorResponse, MonitorResponse, UpdateMonitorRequest, UpdateMonitorResponse, + DeleteMonitorResponse, BadRequestResponse, } from "$lib/types/api"; @@ -143,3 +144,20 @@ export const PATCH: RequestHandler = async ({ locals, request }) => { return json(response); }; + +export const DELETE: RequestHandler = async ({ locals }) => { + // Monitor is validated by middleware and available in locals + const monitor = locals.monitor!; + + // Removes the monitor and everything keyed to its tag: monitoring data, + // incident/maintenance/page links, alerts, alert configs, group + // memberships (with weight rebalancing), and caches. The scheduler drops + // the orphaned BullMQ job on its next reconcile. + await DeleteMonitorCompletelyUsingTag(monitor.tag); + + const response: DeleteMonitorResponse = { + message: `Monitor '${monitor.tag}' deleted successfully`, + }; + + return json(response); +}; diff --git a/src/routes/(docs)/docs/content/v4/api-reference/monitors.md b/src/routes/(docs)/docs/content/v4/api-reference/monitors.md deleted file mode 100644 index 943772cf..00000000 --- a/src/routes/(docs)/docs/content/v4/api-reference/monitors.md +++ /dev/null @@ -1,281 +0,0 @@ ---- -title: Monitors API -description: Create, read, update, and delete monitors via the REST API ---- - -Create, read, update, and delete monitors via the API. - -## List Monitors - -Get all monitors with their current status. - -```http -GET /api/monitors -``` - -### Parameters - -| Parameter | Type | Description | -| --------- | ------- | ------------------------------------ | -| `page` | integer | Page number (default: 1) | -| `limit` | integer | Items per page (default: 20) | -| `status` | string | Filter by status: up, down, degraded | -| `type` | string | Filter by type: API, PING, TCP, etc. | - -### Example Request - -```bash -curl -X GET "https://your-kener.com/api/monitors?status=up&limit=10" \ - -H "Authorization: Bearer YOUR_API_KEY" -``` - -### Example Response - -```json -{ - "success": true, - "data": { - "monitors": [ - { - "id": "mon_abc123", - "name": "API Server", - "type": "API", - "url": "https://api.example.com/health", - "status": "up", - "uptime": 99.95, - "lastChecked": "2024-01-15T10:30:00Z" - } - ], - "pagination": { - "page": 1, - "limit": 10, - "total": 25 - } - } -} -``` - -## Get Monitor - -Get details for a specific monitor. - -```http -GET /api/monitors/:id -``` - -### Example Request - -```bash -curl -X GET https://your-kener.com/api/monitors/mon_abc123 \ - -H "Authorization: Bearer YOUR_API_KEY" -``` - -### Example Response - -```json -{ - "success": true, - "data": { - "id": "mon_abc123", - "name": "API Server", - "type": "API", - "url": "https://api.example.com/health", - "method": "GET", - "expectedStatusCode": 200, - "interval": 60, - "timeout": 10000, - "status": "up", - "uptime": { - "day": 100, - "week": 99.95, - "month": 99.87 - }, - "responseTime": { - "current": 145, - "average": 152 - }, - "lastChecked": "2024-01-15T10:30:00Z", - "createdAt": "2024-01-01T00:00:00Z" - } -} -``` - -## Create Monitor - -Create a new monitor. - -```http -POST /api/monitors -``` - -### Request Body - -```json -{ - "name": "Production API", - "type": "API", - "url": "https://api.example.com/health", - "method": "GET", - "expectedStatusCode": 200, - "interval": 60, - "timeout": 10000, - "headers": { - "Authorization": "Bearer token" - } -} -``` - -### Monitor Types - -#### API Monitor - -```json -{ - "name": "API Health", - "type": "API", - "url": "https://api.example.com", - "method": "GET", - "expectedStatusCode": 200, - "headers": {}, - "body": null -} -``` - -#### Ping Monitor - -```json -{ - "name": "Server Ping", - "type": "PING", - "host": "server.example.com" -} -``` - -#### TCP Monitor - -```json -{ - "name": "Database Port", - "type": "TCP", - "host": "db.example.com", - "port": 5432 -} -``` - -#### DNS Monitor - -```json -{ - "name": "DNS Check", - "type": "DNS", - "host": "example.com", - "recordType": "A" -} -``` - -### Example Request - -```bash -curl -X POST https://your-kener.com/api/monitors \ - -H "Authorization: Bearer YOUR_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Production API", - "type": "API", - "url": "https://api.example.com/health", - "interval": 60 - }' -``` - -### Example Response - -```json -{ - "success": true, - "data": { - "id": "mon_xyz789", - "name": "Production API", - "type": "API", - "status": "pending", - "createdAt": "2024-01-15T10:30:00Z" - }, - "message": "Monitor created successfully" -} -``` - -## Update Monitor - -Update an existing monitor. - -```http -PUT /api/monitors/:id -``` - -### Example Request - -```bash -curl -X PUT https://your-kener.com/api/monitors/mon_abc123 \ - -H "Authorization: Bearer YOUR_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "Updated API Name", - "interval": 120 - }' -``` - -## Delete Monitor - -Delete a monitor. - -```http -DELETE /api/monitors/:id -``` - -### Example Request - -```bash -curl -X DELETE https://your-kener.com/api/monitors/mon_abc123 \ - -H "Authorization: Bearer YOUR_API_KEY" -``` - -## Monitor History - -Get historical data for a monitor. - -```http -GET /api/monitors/:id/history -``` - -### Parameters - -| Parameter | Type | Description | -| ------------ | ------ | ---------------------------------- | -| `start` | string | Start date (ISO 8601) | -| `end` | string | End date (ISO 8601) | -| `resolution` | string | Data resolution: minute, hour, day | - -### Example Request - -```bash -curl -X GET "https://your-kener.com/api/monitors/mon_abc123/history?resolution=hour" \ - -H "Authorization: Bearer YOUR_API_KEY" -``` - -## Pause/Resume Monitor - -```http -POST /api/monitors/:id/pause -POST /api/monitors/:id/resume -``` - -### Example - -```bash -# Pause -curl -X POST https://your-kener.com/api/monitors/mon_abc123/pause \ - -H "Authorization: Bearer YOUR_API_KEY" - -# Resume -curl -X POST https://your-kener.com/api/monitors/mon_abc123/resume \ - -H "Authorization: Bearer YOUR_API_KEY" -``` diff --git a/src/routes/(docs)/docs/content/v4/configuration.md b/src/routes/(docs)/docs/content/v4/configuration.md index fdfdb5bf..dae98fe9 100644 --- a/src/routes/(docs)/docs/content/v4/configuration.md +++ b/src/routes/(docs)/docs/content/v4/configuration.md @@ -196,7 +196,7 @@ status.yourdomain.com { ## Next Steps -- Set up [Monitors](/docs/monitors) -- Configure [Incidents](/docs/incidents) +- Set up [Monitors](/docs/v4/monitors) +- Configure [Incidents](/docs/v4/incidents) - Configure [Sharing Monitors](/docs/v4/sharing) -- Explore the [API Reference](/docs/api-reference) +- Explore the [API Reference](/docs/spec/v4/) diff --git a/src/routes/(docs)/docs/content/v4/pages.md b/src/routes/(docs)/docs/content/v4/pages.md index 4f3fae06..cb5ca2c1 100644 --- a/src/routes/(docs)/docs/content/v4/pages.md +++ b/src/routes/(docs)/docs/content/v4/pages.md @@ -97,6 +97,10 @@ Example: delete services ``` +## Manage pages via API {#manage-pages-via-api} + +Pages support full CRUD through the v4 REST API (`/api/v4/pages`), including assigning monitors. The home page has an empty stored path, so the API addresses it with the special segment `~home` (for example `PATCH /api/v4/pages/~home`); its path cannot be changed and it cannot be deleted. See the [API Reference](/docs/spec/v4/) for endpoints and schemas. + ## Tips {#tips} - Keep page paths short and stable (changing links later is disruptive). diff --git a/static/api-references/v4.json b/static/api-references/v4.json index d49cdc2a..9a88952c 100644 --- a/static/api-references/v4.json +++ b/static/api-references/v4.json @@ -230,6 +230,30 @@ "$ref": "#/components/responses/InternalServerError" } } + }, + "delete": { + "tags": ["Monitors"], + "operationId": "deleteMonitor", + "summary": "Delete monitor", + "description": "Deletes the monitor and everything keyed to its tag: monitoring data, incident/maintenance/page links, alerts, alert configurations, and group memberships (remaining member weights are rebalanced equally). This is irreversible. Scheduled checks stop within seconds.", + "responses": { + "200": { + "description": "Monitor deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } } }, "/api/v4/monitors/{monitor_tag}/data": {