Tell someone when the engine breaks

Everything that goes wrong already travelled the event bus, but the only
subscriber was the editor's websocket — so a flow quarantined at three in
the morning was invisible until someone opened the browser.

An alert manager now watches the same bus and forwards failures to ntfy,
email or a webhook. Most of what it does is decline to send: the same
node failing every second is one alert with a count of what followed, a
connection flapping up and down is muted until it settles, and nothing
gets past ten notifications an hour. Verified against a live instance —
six identical failures produced one alert carrying the real traceback
message.

Channels and rules are configured through the API, with a test send so a
channel can be proven before anything depends on it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
This commit is contained in:
root
2026-08-16 08:01:50 +02:00
co-authored by Claude Fable 5
parent 04329149b3
commit dbdbcc1091
12 changed files with 824 additions and 3 deletions
+82
View File
@@ -1,5 +1,32 @@
// This file is auto-generated by @hey-api/openapi-ts
export const AlertsConfigSchema = {
properties: {
enabled: {
type: 'boolean',
title: 'Enabled',
default: true
},
channels: {
items: {
'$ref': '#/components/schemas/Channel'
},
type: 'array',
title: 'Channels'
},
rules: {
items: {
'$ref': '#/components/schemas/Rule'
},
type: 'array',
title: 'Rules'
}
},
type: 'object',
title: 'AlertsConfig',
description: 'The whole alerting setup, as stored and as the API sees it.'
} as const;
export const Body_login_login_access_tokenSchema = {
properties: {
grant_type: {
@@ -135,6 +162,34 @@ export const Body_oauth_tokenSchema = {
title: 'Body_oauth-token'
} as const;
export const ChannelSchema = {
properties: {
name: {
type: 'string',
title: 'Name'
},
kind: {
type: 'string',
enum: ['ntfy', 'smtp', 'webhook'],
title: 'Kind'
},
enabled: {
type: 'boolean',
title: 'Enabled',
default: true
},
config: {
additionalProperties: true,
type: 'object',
title: 'Config'
}
},
type: 'object',
required: ['name', 'kind'],
title: 'Channel',
description: 'Somewhere to send an alert.'
} as const;
export const DTypeSchema = {
type: 'string',
enum: ['float', 'int', 'str', 'bool', 'json'],
@@ -1013,6 +1068,33 @@ export const RenameRequestSchema = {
title: 'RenameRequest'
} as const;
export const RuleSchema = {
properties: {
events: {
items: {
type: 'string'
},
type: 'array',
title: 'Events'
},
channels: {
items: {
type: 'string'
},
type: 'array',
title: 'Channels'
},
cooldown_s: {
type: 'number',
title: 'Cooldown S',
default: 900
}
},
type: 'object',
title: 'Rule',
description: 'Which events go to which channels.'
} as const;
export const RunRequestSchema = {
properties: {
inputs: {
+57 -1
View File
@@ -3,7 +3,63 @@
import type { CancelablePromise } from './core/CancelablePromise';
import { OpenAPI } from './core/OpenAPI';
import { request as __request } from './core/request';
import type { FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, PrivateCreateUserData, PrivateCreateUserResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse, UtilsHealthResponse } from './types.gen';
import type { AlertsReadAlertsConfigResponse, AlertsSaveAlertsConfigData, AlertsSaveAlertsConfigResponse, AlertsTestChannelData, AlertsTestChannelResponse, FlowsReadFlowsResponse, FlowsReadNodeTypesResponse, FlowsReadLibraryResponse, FlowsDeleteSharedNodeData, FlowsDeleteSharedNodeResponse, FlowsReadFlowData, FlowsReadFlowResponse, FlowsSaveFlowData, FlowsSaveFlowResponse, FlowsDeleteFlowData, FlowsDeleteFlowResponse, FlowsPublishFlowData, FlowsPublishFlowResponse, FlowsDiscardDraftData, FlowsDiscardDraftResponse, FlowsRenameFlowData, FlowsRenameFlowResponse, FlowsReadNodeSourceData, FlowsReadNodeSourceResponse, FlowsSaveNodeSourceData, FlowsSaveNodeSourceResponse, FlowsShareNodeData, FlowsShareNodeResponse, FlowsUnshareNodeData, FlowsUnshareNodeResponse, FlowsStartFlowData, FlowsStartFlowResponse, FlowsStopFlowData, FlowsStopFlowResponse, FlowsPauseFlowData, FlowsPauseFlowResponse, FlowsResumeFlowData, FlowsResumeFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, PrivateCreateUserData, PrivateCreateUserResponse, SecretsReadSecretsResponse, SecretsSaveSecretData, SecretsSaveSecretResponse, SecretsDeleteSecretData, SecretsDeleteSecretResponse, UsersReadUsersData, UsersReadUsersResponse, UsersCreateUserData, UsersCreateUserResponse, UsersReadUserMeResponse, UsersDeleteUserMeResponse, UsersUpdateUserMeData, UsersUpdateUserMeResponse, UsersUpdatePasswordMeData, UsersUpdatePasswordMeResponse, UsersRegisterUserData, UsersRegisterUserResponse, UsersReadUserByIdData, UsersReadUserByIdResponse, UsersUpdateUserData, UsersUpdateUserResponse, UsersDeleteUserData, UsersDeleteUserResponse, UtilsTestEmailData, UtilsTestEmailResponse, UtilsHealthCheckResponse, UtilsHealthResponse } from './types.gen';
export class AlertsService {
/**
* Read Alerts Config
* What the engine alerts on, and where it sends it.
* @returns AlertsConfig Successful Response
* @throws ApiError
*/
public static readAlertsConfig(): CancelablePromise<AlertsReadAlertsConfigResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/alerts/config'
});
}
/**
* Save Alerts Config
* Replace the configuration. Takes effect immediately.
* @param data The data for the request.
* @param data.requestBody
* @returns AlertsConfig Successful Response
* @throws ApiError
*/
public static saveAlertsConfig(data: AlertsSaveAlertsConfigData): CancelablePromise<AlertsSaveAlertsConfigResponse> {
return __request(OpenAPI, {
method: 'PUT',
url: '/api/v1/alerts/config',
body: data.requestBody,
mediaType: 'application/json',
errors: {
422: 'Validation Error'
}
});
}
/**
* Test Channel
* Send one alert, to prove the channel works before relying on it.
* @param data The data for the request.
* @param data.channelName
* @returns Message Successful Response
* @throws ApiError
*/
public static testChannel(data: AlertsTestChannelData): CancelablePromise<AlertsTestChannelResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/api/v1/alerts/test/{channel_name}',
path: {
channel_name: data.channelName
},
errors: {
422: 'Validation Error'
}
});
}
}
export class FlowsService {
/**
+46
View File
@@ -1,5 +1,14 @@
// This file is auto-generated by @hey-api/openapi-ts
/**
* The whole alerting setup, as stored and as the API sees it.
*/
export type AlertsConfig = {
enabled?: boolean;
channels?: Array<Channel>;
rules?: Array<Rule>;
};
export type Body_login_login_access_token = {
grant_type?: (string | null);
username: string;
@@ -19,6 +28,20 @@ export type Body_oauth_token = {
resource?: (string | null);
};
/**
* Somewhere to send an alert.
*/
export type Channel = {
name: string;
kind: 'ntfy' | 'smtp' | 'webhook';
enabled?: boolean;
config?: {
[key: string]: unknown;
};
};
export type kind = 'ntfy' | 'smtp' | 'webhook';
/**
* Serializable payload types.
*
@@ -301,6 +324,15 @@ export type RenameRequest = {
new_name: string;
};
/**
* Which events go to which channels.
*/
export type Rule = {
events?: Array<(string)>;
channels?: Array<(string)>;
cooldown_s?: number;
};
export type RunRequest = {
inputs?: {
[key: string]: unknown;
@@ -402,6 +434,20 @@ export type ValidationResult = {
issues?: Array<ValidationIssue>;
};
export type AlertsReadAlertsConfigResponse = (AlertsConfig);
export type AlertsSaveAlertsConfigData = {
requestBody: AlertsConfig;
};
export type AlertsSaveAlertsConfigResponse = (AlertsConfig);
export type AlertsTestChannelData = {
channelName: string;
};
export type AlertsTestChannelResponse = (Message);
export type FlowsReadFlowsResponse = (FlowsPublic);
export type FlowsReadNodeTypesResponse = (Array<NodeTypeInfo>);