diff --git a/migrations/0009_shocking_butterfly.sql b/migrations/0009_shocking_butterfly.sql new file mode 100644 index 0000000..19fdff5 --- /dev/null +++ b/migrations/0009_shocking_butterfly.sql @@ -0,0 +1,8 @@ +ALTER TABLE `checks` ADD `degraded` integer DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `monitors` ADD `expect_keyword` text;--> statement-breakpoint +ALTER TABLE `monitors` ADD `keyword_inverted` integer DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `monitors` ADD `request_headers` text;--> statement-breakpoint +ALTER TABLE `monitors` ADD `request_body` text;--> statement-breakpoint +ALTER TABLE `monitors` ADD `degraded_latency_ms` integer;--> statement-breakpoint +ALTER TABLE `monitors` ADD `consecutive_slow` integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `monitors` ADD `last_degraded` integer DEFAULT false NOT NULL; \ No newline at end of file diff --git a/migrations/meta/0009_snapshot.json b/migrations/meta/0009_snapshot.json new file mode 100644 index 0000000..5c52d33 --- /dev/null +++ b/migrations/meta/0009_snapshot.json @@ -0,0 +1,1280 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "fdb7ba86-a907-43e1-bcbb-9cf7cbd1cb15", + "prevId": "df28e3d3-6b1a-4060-a848-2067dfd1974c", + "tables": { + "admin_credentials": { + "name": "admin_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ai_settings": { + "name": "ai_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "checks": { + "name": "checks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "checked_at": { + "name": "checked_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "maintenance": { + "name": "maintenance", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "degraded": { + "name": "degraded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "checks_monitor_id_checked_at_idx": { + "name": "checks_monitor_id_checked_at_idx", + "columns": [ + "monitor_id", + "checked_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "checks_monitor_id_monitors_id_fk": { + "name": "checks_monitor_id_monitors_id_fk", + "tableFrom": "checks", + "tableTo": "monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "incident_monitors": { + "name": "incident_monitors", + "columns": { + "incident_id": { + "name": "incident_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "incident_monitors_monitor_id_idx": { + "name": "incident_monitors_monitor_id_idx", + "columns": [ + "monitor_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "incident_monitors_incident_id_incidents_id_fk": { + "name": "incident_monitors_incident_id_incidents_id_fk", + "tableFrom": "incident_monitors", + "tableTo": "incidents", + "columnsFrom": [ + "incident_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "incident_monitors_monitor_id_monitors_id_fk": { + "name": "incident_monitors_monitor_id_monitors_id_fk", + "tableFrom": "incident_monitors", + "tableTo": "monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "incident_monitors_incident_id_monitor_id_pk": { + "columns": [ + "incident_id", + "monitor_id" + ], + "name": "incident_monitors_incident_id_monitor_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "incident_updates": { + "name": "incident_updates", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "incident_id": { + "name": "incident_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "incident_updates_incident_id_created_at_idx": { + "name": "incident_updates_incident_id_created_at_idx", + "columns": [ + "incident_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "incident_updates_incident_id_incidents_id_fk": { + "name": "incident_updates_incident_id_incidents_id_fk", + "tableFrom": "incident_updates", + "tableTo": "incidents", + "columnsFrom": [ + "incident_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "incidents": { + "name": "incidents", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'investigating'" + }, + "impact": { + "name": "impact", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'major'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'auto'" + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_status_code": { + "name": "start_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start_error": { + "name": "start_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "incidents_started_at_idx": { + "name": "incidents_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + }, + "incidents_resolved_at_idx": { + "name": "incidents_resolved_at_idx", + "columns": [ + "resolved_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "login_attempts": { + "name": "login_attempts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempted_at": { + "name": "attempted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "login_attempts_ip_attempted_at_idx": { + "name": "login_attempts_ip_attempted_at_idx", + "columns": [ + "ip_address", + "attempted_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "maintenance_window_monitors": { + "name": "maintenance_window_monitors", + "columns": { + "window_id": { + "name": "window_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "maintenance_window_monitors_monitor_id_idx": { + "name": "maintenance_window_monitors_monitor_id_idx", + "columns": [ + "monitor_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "maintenance_window_monitors_window_id_maintenance_windows_id_fk": { + "name": "maintenance_window_monitors_window_id_maintenance_windows_id_fk", + "tableFrom": "maintenance_window_monitors", + "tableTo": "maintenance_windows", + "columnsFrom": [ + "window_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "maintenance_window_monitors_monitor_id_monitors_id_fk": { + "name": "maintenance_window_monitors_monitor_id_monitors_id_fk", + "tableFrom": "maintenance_window_monitors", + "tableTo": "monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "maintenance_window_monitors_window_id_monitor_id_pk": { + "columns": [ + "window_id", + "monitor_id" + ], + "name": "maintenance_window_monitors_window_id_monitor_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "maintenance_windows": { + "name": "maintenance_windows", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_minute": { + "name": "start_minute", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'UTC'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "maintenance_windows_enabled_idx": { + "name": "maintenance_windows_enabled_idx", + "columns": [ + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitor_daily_stats": { + "name": "monitor_daily_stats", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "day": { + "name": "day", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_checks": { + "name": "total_checks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "up_checks": { + "name": "up_checks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avg_latency_ms": { + "name": "avg_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "min_latency_ms": { + "name": "min_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_latency_ms": { + "name": "max_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "monitor_daily_stats_monitor_id_day_uidx": { + "name": "monitor_daily_stats_monitor_id_day_uidx", + "columns": [ + "monitor_id", + "day" + ], + "isUnique": true + } + }, + "foreignKeys": { + "monitor_daily_stats_monitor_id_monitors_id_fk": { + "name": "monitor_daily_stats_monitor_id_monitors_id_fk", + "tableFrom": "monitor_daily_stats", + "tableTo": "monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitors": { + "name": "monitors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'GET'" + }, + "expected_status": { + "name": "expected_status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 200 + }, + "expect_keyword": { + "name": "expect_keyword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "keyword_inverted": { + "name": "keyword_inverted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "request_headers": { + "name": "request_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_body": { + "name": "request_body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "degraded_latency_ms": { + "name": "degraded_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "timeout_ms": { + "name": "timeout_ms", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10000 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "alerts_enabled": { + "name": "alerts_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "failure_threshold": { + "name": "failure_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2 + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "consecutive_slow": { + "name": "consecutive_slow", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_ok": { + "name": "last_ok", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_degraded": { + "name": "last_degraded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "last_status_code": { + "name": "last_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_latency_ms": { + "name": "last_latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "monitors_enabled_last_checked_at_idx": { + "name": "monitors_enabled_last_checked_at_idx", + "columns": [ + "enabled", + "last_checked_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channel_monitors": { + "name": "notification_channel_monitors", + "columns": { + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "notification_channel_monitors_monitor_id_idx": { + "name": "notification_channel_monitors_monitor_id_idx", + "columns": [ + "monitor_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "notification_channel_monitors_channel_id_notification_channels_id_fk": { + "name": "notification_channel_monitors_channel_id_notification_channels_id_fk", + "tableFrom": "notification_channel_monitors", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_channel_monitors_monitor_id_monitors_id_fk": { + "name": "notification_channel_monitors_monitor_id_monitors_id_fk", + "tableFrom": "notification_channel_monitors", + "tableTo": "monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "notification_channel_monitors_channel_id_monitor_id_pk": { + "columns": [ + "channel_id", + "monitor_id" + ], + "name": "notification_channel_monitors_channel_id_monitor_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "notify_manual": { + "name": "notify_manual", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "notification_channels_enabled_idx": { + "name": "notification_channels_enabled_idx", + "columns": [ + "enabled" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_deliveries": { + "name": "notification_deliveries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "incident_id": { + "name": "incident_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ok": { + "name": "ok", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "notification_deliveries_channel_id_created_at_idx": { + "name": "notification_deliveries_channel_id_created_at_idx", + "columns": [ + "channel_id", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "notification_deliveries_channel_id_notification_channels_id_fk": { + "name": "notification_deliveries_channel_id_notification_channels_id_fk", + "tableFrom": "notification_deliveries", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_deliveries_incident_id_incidents_id_fk": { + "name": "notification_deliveries_incident_id_incidents_id_fk", + "tableFrom": "notification_deliveries", + "tableTo": "incidents", + "columnsFrom": [ + "incident_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "notification_deliveries_monitor_id_monitors_id_fk": { + "name": "notification_deliveries_monitor_id_monitors_id_fk", + "tableFrom": "notification_deliveries", + "tableTo": "monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 8322a06..da385e6 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1788059207578, "tag": "0008_aromatic_chat", "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1788062139636, + "tag": "0009_shocking_butterfly", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/client/api/monitors.ts b/src/client/api/monitors.ts index 3b8737e..72c5dd1 100644 --- a/src/client/api/monitors.ts +++ b/src/client/api/monitors.ts @@ -8,6 +8,11 @@ export type Monitor = { url: string; method: MonitorMethod; expectedStatus: number; + expectKeyword: string | null; + keywordInverted: boolean; + requestHeaders: string | null; + requestBody: string | null; + degradedLatencyMs: number | null; intervalSeconds: number; timeoutMs: number; enabled: boolean; @@ -15,7 +20,9 @@ export type Monitor = { retryCount: number; failureThreshold: number; consecutiveFailures: number; + consecutiveSlow: number; lastOk: boolean | null; + lastDegraded: boolean; lastStatusCode: number | null; lastLatencyMs: number | null; lastError: string | null; @@ -29,6 +36,11 @@ export type MonitorInput = { url: string; method: MonitorMethod; expectedStatus: number; + expectKeyword?: string | null; + keywordInverted?: boolean; + requestHeaders?: Record | null; + requestBody?: string | null; + degradedLatencyMs?: number | null; intervalSeconds: number; timeoutMs: number; retryCount?: number; @@ -39,6 +51,7 @@ export type MonitorInput = { export type CheckResult = { ok: boolean; + degraded: boolean; statusCode: number | null; latencyMs: number; error: string | null; @@ -49,6 +62,7 @@ export type Check = { id: number; monitorId: number; ok: boolean; + degraded: boolean; statusCode: number | null; latencyMs: number; error: string | null; @@ -57,6 +71,7 @@ export type Check = { }; export type CheckTransition = 'opened' | 'pending' | 'cleared' | 'resolved' | null; +export type LatencyTransition = 'degraded' | 'recovered' | null; export type Incident = { id: number; @@ -122,5 +137,7 @@ export function deleteMonitor(id: number) { } export function runMonitorCheck(id: number) { - return postJson<{ result: CheckResult; transition: CheckTransition; monitor: Monitor }>(`/api/monitors/${id}/check`); + return postJson<{ result: CheckResult; transition: CheckTransition; latencyTransition: LatencyTransition; monitor: Monitor }>( + `/api/monitors/${id}/check`, + ); } diff --git a/src/client/api/status.ts b/src/client/api/status.ts index 451b9f0..d789119 100644 --- a/src/client/api/status.ts +++ b/src/client/api/status.ts @@ -1,6 +1,6 @@ import { getJson } from './http'; -export type PublicServiceStatus = 'up' | 'down' | 'unknown' | 'maintenance'; +export type PublicServiceStatus = 'up' | 'degraded' | 'down' | 'unknown' | 'maintenance'; export type PublicOverallStatus = 'operational' | 'degraded' | 'down'; export type PublicIncidentUpdate = { status: string; body: string; createdAt: string }; diff --git a/src/client/components/charts/UptimeBar.tsx b/src/client/components/charts/UptimeBar.tsx index 8a60cd8..2a7b87f 100644 --- a/src/client/components/charts/UptimeBar.tsx +++ b/src/client/components/charts/UptimeBar.tsx @@ -12,7 +12,7 @@ type UptimeDatum = { id: string; successfulChecks: number; totalChecks: number; - status: 'up' | 'down'; + status: 'up' | 'degraded' | 'down'; fill: string; }; @@ -20,6 +20,7 @@ const MAX_VISIBLE_SEGMENTS = 32; const uptimeConfig = { up: { label: 'Up', color: 'var(--primary)' }, + degraded: { label: 'Degraded', color: 'var(--chart-degraded)' }, down: { label: 'Down', color: 'var(--chart-down)' }, } satisfies ChartConfig; @@ -58,6 +59,8 @@ function groupChecks(checks: Check[]): UptimeDatum[] { const last = bucket[bucket.length - 1]; const successfulChecks = bucket.filter((check) => check.ok).length; const ok = successfulChecks === bucket.length; + const degraded = ok && bucket.some((check) => check.degraded); + const status = !ok ? 'down' : degraded ? 'degraded' : 'up'; return { startTime: first.checkedAt, @@ -67,8 +70,8 @@ function groupChecks(checks: Check[]): UptimeDatum[] { id: `${first.id}-${last.id}`, successfulChecks, totalChecks: bucket.length, - status: ok ? 'up' : 'down', - fill: ok ? 'var(--color-up)' : 'var(--color-down)', + status, + fill: `var(--color-${status})`, }; }); } @@ -114,7 +117,7 @@ export function UptimeBar({ checks }: { checks: Check[] }) { onAnimationEnd={() => setHasAnimated(true)} > {data.map((point) => ( - + ))} @@ -123,7 +126,7 @@ export function UptimeBar({ checks }: { checks: Check[] }) {
Oldest - Up Down + Up Degraded Down Latest
diff --git a/src/client/components/dashboard/MonitorFormDialog.tsx b/src/client/components/dashboard/MonitorFormDialog.tsx index a1410e0..6f5855e 100644 --- a/src/client/components/dashboard/MonitorFormDialog.tsx +++ b/src/client/components/dashboard/MonitorFormDialog.tsx @@ -1,4 +1,5 @@ import { type FormEvent, useState } from 'react'; +import { ChevronDown, Plus, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; @@ -16,7 +17,13 @@ export const DEFAULT_MONITOR_INPUT: MonitorInput = { timeoutMs: 10_000, retryCount: 1, failureThreshold: 2, + expectKeyword: null, + keywordInverted: false, + requestHeaders: null, + requestBody: null, + degradedLatencyMs: null, enabled: true, + alertsEnabled: true, }; export const INTERVAL_OPTIONS = [ @@ -32,6 +39,17 @@ type MonitorFormDialogProps = { onClose: () => void; }; +type HeaderRow = { id: number; name: string; value: string }; + +function headerRows(requestHeaders: string | null): HeaderRow[] { + if (!requestHeaders) return []; + try { + return Object.entries(JSON.parse(requestHeaders) as Record).map(([name, value], index) => ({ id: index, name, value })); + } catch { + return []; + } +} + function errorMessage(error: unknown, fallback: string) { return error instanceof Error ? error.message : fallback; } @@ -47,6 +65,11 @@ function monitorInput(monitor: Monitor | null): MonitorInput { timeoutMs: monitor.timeoutMs, retryCount: monitor.retryCount, failureThreshold: monitor.failureThreshold, + expectKeyword: monitor.expectKeyword, + keywordInverted: monitor.keywordInverted, + requestHeaders: monitor.requestHeaders ? (JSON.parse(monitor.requestHeaders) as Record) : null, + requestBody: monitor.requestBody, + degradedLatencyMs: monitor.degradedLatencyMs, enabled: monitor.enabled, alertsEnabled: monitor.alertsEnabled, }; @@ -56,6 +79,8 @@ export function MonitorFormDialog({ editing, onClose }: MonitorFormDialogProps) const createMutation = useCreateMonitorMutation(); const updateMutation = useUpdateMonitorMutation(); const [form, setForm] = useState(() => monitorInput(editing)); + const [headers, setHeaders] = useState(() => headerRows(editing?.requestHeaders ?? null)); + const [nextHeaderId, setNextHeaderId] = useState(() => headers.length); const formMutation = editing ? updateMutation : createMutation; function closeForm() { @@ -65,13 +90,31 @@ export function MonitorFormDialog({ editing, onClose }: MonitorFormDialogProps) function handleSubmit(event: FormEvent) { event.preventDefault(); + const requestHeaders = Object.fromEntries( + headers.filter((header) => header.name.trim()).map((header) => [header.name.trim(), header.value]), + ); + const input: MonitorInput = { + ...form, + expectKeyword: form.expectKeyword?.trim() || null, + requestHeaders: Object.keys(requestHeaders).length > 0 ? requestHeaders : null, + requestBody: form.method === 'POST' ? form.requestBody || null : null, + }; if (editing) { - updateMutation.mutate({ id: editing.id, input: form }, { onSuccess: onClose }); + updateMutation.mutate({ id: editing.id, input }, { onSuccess: onClose }); } else { - createMutation.mutate(form, { onSuccess: onClose }); + createMutation.mutate(input, { onSuccess: onClose }); } } + function addHeader() { + setHeaders((current) => [...current, { id: nextHeaderId, name: '', value: '' }]); + setNextHeaderId((current) => current + 1); + } + + function updateHeader(id: number, field: 'name' | 'value', value: string) { + setHeaders((current) => current.map((header) => (header.id === id ? { ...header, [field]: value } : header))); + } + return ( +
+ + Advanced request and response checks + +
+ +
+ setForm({ ...form, keywordInverted })} + disabled={!form.expectKeyword} + /> + +
+ +
+
+
+ Request headers + Up to 10 headers. Restricted transport headers are blocked. +
+ +
+ {headers.length > 0 && ( +
+ {headers.map((header) => ( +
+ updateHeader(header.id, 'name', event.target.value)} + maxLength={64} + placeholder="Authorization" + /> + updateHeader(header.id, 'value', event.target.value)} + maxLength={512} + placeholder="Bearer …" + /> + +
+ ))} +
+ )} +
+ {form.method === 'POST' && ( +