Keep the engine's own history, and a screen that reads it

A second bus subscriber folds executions, errors, timings and queue lag
into per-minute rollups, keeps failures with their traceback and an audit
trail of who published what, and records one row per cascade — manual runs
and previews included, under an id of their own that writes no idempotency
markers. Read back through /observability/*, which always answers 200 so a
degraded engine still renders its own health screen.

Also fixes two things found on the way: node-health alerts read `status`
where the engine publishes `health`, so a device dropping never alerted
anyone, and the Redis queue reported `parked: 0` whatever was held.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
This commit is contained in:
2026-08-16 22:29:32 +02:00
co-authored by Claude Fable 5
parent f300c43f3a
commit af3ba51571
30 changed files with 2610 additions and 22 deletions
+261
View File
@@ -379,6 +379,38 @@ export const DashboardsPublicSchema = {
title: 'DashboardsPublic'
} as const;
export const DeadLetterSchema = {
properties: {
id: {
type: 'string',
title: 'Id'
},
ts: {
type: 'number',
title: 'Ts'
},
flow: {
type: 'string',
title: 'Flow'
},
node: {
type: 'string',
title: 'Node'
},
cause: {
type: 'string',
title: 'Cause'
},
reason: {
type: 'string',
title: 'Reason'
}
},
type: 'object',
required: ['id', 'ts', 'flow', 'node', 'cause', 'reason'],
title: 'DeadLetter'
} as const;
export const EndpointSchema = {
properties: {
kind: {
@@ -426,6 +458,43 @@ these so a value never appears to come from nowhere — or worse, appears to
come from whichever node happens to be drawn as a producer.`
} as const;
export const EventRowSchema = {
properties: {
id: {
type: 'integer',
title: 'Id'
},
ts: {
type: 'string',
format: 'date-time',
title: 'Ts'
},
type: {
type: 'string',
title: 'Type'
},
flow: {
type: 'string',
title: 'Flow'
},
node: {
type: 'string',
title: 'Node'
},
detail: {
type: 'string',
title: 'Detail'
},
actor: {
type: 'string',
title: 'Actor'
}
},
type: 'object',
required: ['id', 'ts', 'type', 'flow', 'node', 'detail', 'actor'],
title: 'EventRow'
} as const;
export const FlowDef_InputSchema = {
properties: {
name: {
@@ -597,6 +666,56 @@ export const FlowInput_OutputSchema = {
description: 'A message the flow starts with rather than computes.'
} as const;
export const FlowRollupSchema = {
properties: {
flow: {
type: 'string',
title: 'Flow'
},
executions: {
type: 'integer',
title: 'Executions'
},
errors: {
type: 'integer',
title: 'Errors'
},
messages: {
type: 'integer',
title: 'Messages'
},
avg_ms: {
type: 'number',
title: 'Avg Ms'
},
avg_lag_ms: {
type: 'number',
title: 'Avg Lag Ms'
},
spark: {
items: {
type: 'integer'
},
type: 'array',
title: 'Spark'
},
last_error_ts: {
anyOf: [
{
type: 'number'
},
{
type: 'null'
}
],
title: 'Last Error Ts'
}
},
type: 'object',
required: ['flow', 'executions', 'errors', 'messages', 'avg_ms', 'avg_lag_ms', 'spark'],
title: 'FlowRollup'
} as const;
export const FlowStatePublicSchema = {
properties: {
values: {
@@ -698,6 +817,55 @@ export const HTTPValidationErrorSchema = {
title: 'HTTPValidationError'
} as const;
export const HealthSummarySchema = {
properties: {
status: {
type: 'string',
title: 'Status'
},
problems: {
items: {
type: 'string'
},
type: 'array',
title: 'Problems'
},
flows: {
additionalProperties: {
type: 'integer'
},
type: 'object',
title: 'Flows'
},
nodes: {
additionalProperties: {
type: 'integer'
},
type: 'object',
title: 'Nodes'
},
queue: {
additionalProperties: true,
type: 'object',
title: 'Queue'
},
loop_lag: {
additionalProperties: {
type: 'number'
},
type: 'object',
title: 'Loop Lag'
},
failures_24h: {
type: 'integer',
title: 'Failures 24H'
}
},
type: 'object',
required: ['status', 'problems', 'flows', 'nodes', 'queue', 'loop_lag', 'failures_24h'],
title: 'HealthSummary'
} as const;
export const HistoryPointSchema = {
properties: {
ts: {
@@ -1630,6 +1798,63 @@ export const RunRequestSchema = {
title: 'RunRequest'
} as const;
export const RunRowSchema = {
properties: {
id: {
type: 'string',
title: 'Id'
},
flow: {
type: 'string',
title: 'Flow'
},
source: {
type: 'string',
title: 'Source'
},
status: {
type: 'string',
title: 'Status'
},
started_at: {
type: 'string',
format: 'date-time',
title: 'Started At'
},
finished_at: {
anyOf: [
{
type: 'string',
format: 'date-time'
},
{
type: 'null'
}
],
title: 'Finished At'
},
nodes: {
type: 'integer',
title: 'Nodes'
},
errors: {
type: 'integer',
title: 'Errors'
},
duration_ms: {
type: 'number',
title: 'Duration Ms'
},
deliveries: {
type: 'integer',
title: 'Deliveries'
}
},
type: 'object',
required: ['id', 'flow', 'source', 'status', 'started_at', 'nodes', 'errors', 'duration_ms', 'deliveries'],
title: 'RunRow'
} as const;
export const SecretNamesSchema = {
properties: {
data: {
@@ -1711,6 +1936,42 @@ export const SectionDef_OutputSchema = {
description: 'A grid of widgets under a heading.'
} as const;
export const SeriesPointSchema = {
properties: {
ts: {
type: 'number',
title: 'Ts'
},
executions: {
type: 'integer',
title: 'Executions'
},
errors: {
type: 'integer',
title: 'Errors'
},
messages: {
type: 'integer',
title: 'Messages'
},
avg_ms: {
type: 'number',
title: 'Avg Ms'
},
max_ms: {
type: 'number',
title: 'Max Ms'
},
avg_lag_ms: {
type: 'number',
title: 'Avg Lag Ms'
}
},
type: 'object',
required: ['ts', 'executions', 'errors', 'messages', 'avg_ms', 'max_ms', 'avg_lag_ms'],
title: 'SeriesPoint'
} as const;
export const ShareRequestSchema = {
properties: {
lib_name: {
+135 -1
View File
@@ -3,7 +3,7 @@
import type { CancelablePromise } from './core/CancelablePromise';
import { OpenAPI } from './core/OpenAPI';
import { request as __request } from './core/request';
import type { AlertsReadAlertsConfigResponse, AlertsSaveAlertsConfigData, AlertsSaveAlertsConfigResponse, AlertsTestChannelData, AlertsTestChannelResponse, DashboardsReadDashboardsResponse, DashboardsReadDashboardData, DashboardsReadDashboardResponse, DashboardsCreateDashboardData, DashboardsCreateDashboardResponse, DashboardsSaveDashboardData, DashboardsSaveDashboardResponse, DashboardsDeleteDashboardData, DashboardsDeleteDashboardResponse, DashboardsRenameDashboardData, DashboardsRenameDashboardResponse, 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, FlowsStepFlowData, FlowsStepFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsCancelNodeData, FlowsCancelNodeResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, MessagesReadMessagesResponse, MessagesPublishMessageData, MessagesPublishMessageResponse, MessagesReadMessageHistoryData, MessagesReadMessageHistoryResponse, ModulesReadModulesResponse, ModulesApplyModulesData, ModulesApplyModulesResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, OauthReadClientsResponse, OauthRevokeClientData, OauthRevokeClientResponse, 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, DashboardsReadDashboardsResponse, DashboardsReadDashboardData, DashboardsReadDashboardResponse, DashboardsCreateDashboardData, DashboardsCreateDashboardResponse, DashboardsSaveDashboardData, DashboardsSaveDashboardResponse, DashboardsDeleteDashboardData, DashboardsDeleteDashboardResponse, DashboardsRenameDashboardData, DashboardsRenameDashboardResponse, 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, FlowsStepFlowData, FlowsStepFlowResponse, FlowsValidateFlowData, FlowsValidateFlowResponse, FlowsRunFlowData, FlowsRunFlowResponse, FlowsTriggerNodeData, FlowsTriggerNodeResponse, FlowsCancelNodeData, FlowsCancelNodeResponse, FlowsReadFlowStateData, FlowsReadFlowStateResponse, FlowsReadMessageHistoryData, FlowsReadMessageHistoryResponse, LoginLoginAccessTokenData, LoginLoginAccessTokenResponse, LoginTestTokenResponse, LoginRecoverPasswordData, LoginRecoverPasswordResponse, LoginResetPasswordData, LoginResetPasswordResponse, LoginRecoverPasswordHtmlContentData, LoginRecoverPasswordHtmlContentResponse, MessagesReadMessagesResponse, MessagesPublishMessageData, MessagesPublishMessageResponse, MessagesReadMessageHistoryData, MessagesReadMessageHistoryResponse, ModulesReadModulesResponse, ModulesApplyModulesData, ModulesApplyModulesResponse, OauthRegisterClientData, OauthRegisterClientResponse, OauthAuthorizeValidateData, OauthAuthorizeValidateResponse, OauthAuthorizeData, OauthAuthorizeResponse, OauthTokenData, OauthTokenResponse, OauthReadClientsResponse, OauthRevokeClientData, OauthRevokeClientResponse, ObservabilityReadSummaryResponse, ObservabilityReadTimeseriesData, ObservabilityReadTimeseriesResponse, ObservabilityReadFlowRollupsData, ObservabilityReadFlowRollupsResponse, ObservabilityReadRunsData, ObservabilityReadRunsResponse, ObservabilityReadEventsData, ObservabilityReadEventsResponse, ObservabilityReadDeadLettersData, ObservabilityReadDeadLettersResponse, 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 {
/**
@@ -1069,6 +1069,140 @@ export class OauthService {
}
}
export class ObservabilityService {
/**
* Read Summary
* How the engine is doing right now. Always 200, degraded or not.
* @returns HealthSummary Successful Response
* @throws ApiError
*/
public static readSummary(): CancelablePromise<ObservabilityReadSummaryResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/observability/summary'
});
}
/**
* Read Timeseries
* Executions, errors and timings over time, summed across nodes.
* @param data The data for the request.
* @param data.flow
* @param data.node
* @param data.hours
* @param data.bucketS
* @returns SeriesPoint Successful Response
* @throws ApiError
*/
public static readTimeseries(data: ObservabilityReadTimeseriesData = {}): CancelablePromise<ObservabilityReadTimeseriesResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/observability/timeseries',
query: {
flow: data.flow,
node: data.node,
hours: data.hours,
bucket_s: data.bucketS
},
errors: {
422: 'Validation Error'
}
});
}
/**
* Read Flow Rollups
* One row per flow, with a coarse trend of how much it ran.
* @param data The data for the request.
* @param data.hours
* @returns FlowRollup Successful Response
* @throws ApiError
*/
public static readFlowRollups(data: ObservabilityReadFlowRollupsData = {}): CancelablePromise<ObservabilityReadFlowRollupsResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/observability/flows',
query: {
hours: data.hours
},
errors: {
422: 'Validation Error'
}
});
}
/**
* Read Runs
* Recent cascades, newest first.
* @param data The data for the request.
* @param data.flow
* @param data.status
* @param data.limit
* @returns RunRow Successful Response
* @throws ApiError
*/
public static readRuns(data: ObservabilityReadRunsData = {}): CancelablePromise<ObservabilityReadRunsResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/observability/runs',
query: {
flow: data.flow,
status: data.status,
limit: data.limit
},
errors: {
422: 'Validation Error'
}
});
}
/**
* Read Events
* What went wrong, or who changed what. Newest first.
* @param data The data for the request.
* @param data.kind
* @param data.flow
* @param data.limit
* @returns EventRow Successful Response
* @throws ApiError
*/
public static readEvents(data: ObservabilityReadEventsData = {}): CancelablePromise<ObservabilityReadEventsResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/observability/events',
query: {
kind: data.kind,
flow: data.flow,
limit: data.limit
},
errors: {
422: 'Validation Error'
}
});
}
/**
* Read Dead Letters
* Work the engine gave up on, which nothing else surfaces.
* @param data The data for the request.
* @param data.limit
* @returns DeadLetter Successful Response
* @throws ApiError
*/
public static readDeadLetters(data: ObservabilityReadDeadLettersData = {}): CancelablePromise<ObservabilityReadDeadLettersResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/api/v1/observability/dead-letter',
query: {
limit: data.limit
},
errors: {
422: 'Validation Error'
}
});
}
}
export class PrivateService {
/**
* Create User
+110
View File
@@ -129,6 +129,15 @@ export type DashboardSummary = {
widget_count?: number;
};
export type DeadLetter = {
id: string;
ts: number;
flow: string;
node: string;
cause: string;
reason: string;
};
/**
* Serializable payload types.
*
@@ -154,6 +163,16 @@ export type Endpoint = {
requires?: Array<(string)>;
};
export type EventRow = {
id: number;
ts: string;
type: string;
flow: string;
node: string;
detail: string;
actor: string;
};
/**
* One atomic flow.
*/
@@ -209,6 +228,17 @@ export type FlowInput_Output = {
initial?: (unknown | null);
};
export type FlowRollup = {
flow: string;
executions: number;
errors: number;
messages: number;
avg_ms: number;
avg_lag_ms: number;
spark: Array<(number)>;
last_error_ts?: (number | null);
};
export type FlowsPublic = {
data: Array<FlowSummary>;
count: number;
@@ -232,6 +262,24 @@ export type FlowSummary = {
quarantined?: boolean;
};
export type HealthSummary = {
status: string;
problems: Array<(string)>;
flows: {
[key: string]: (number);
};
nodes: {
[key: string]: (number);
};
queue: {
[key: string]: unknown;
};
loop_lag: {
[key: string]: (number);
};
failures_24h: number;
};
/**
* One numeric value a message carried, and when.
*/
@@ -530,6 +578,19 @@ export type RunRequest = {
};
};
export type RunRow = {
id: string;
flow: string;
source: string;
status: string;
started_at: string;
finished_at?: (string | null);
nodes: number;
errors: number;
duration_ms: number;
deliveries: number;
};
export type SecretNames = {
data: Array<(string)>;
count: number;
@@ -557,6 +618,16 @@ export type SectionDef_Output = {
widgets?: Array<WidgetDef>;
};
export type SeriesPoint = {
ts: number;
executions: number;
errors: number;
messages: number;
avg_ms: number;
max_ms: number;
avg_lag_ms: number;
};
export type ShareRequest = {
lib_name: string;
};
@@ -946,6 +1017,45 @@ export type OauthRevokeClientData = {
export type OauthRevokeClientResponse = (Message);
export type ObservabilityReadSummaryResponse = (HealthSummary);
export type ObservabilityReadTimeseriesData = {
bucketS?: number;
flow?: (string | null);
hours?: number;
node?: (string | null);
};
export type ObservabilityReadTimeseriesResponse = (Array<SeriesPoint>);
export type ObservabilityReadFlowRollupsData = {
hours?: number;
};
export type ObservabilityReadFlowRollupsResponse = (Array<FlowRollup>);
export type ObservabilityReadRunsData = {
flow?: (string | null);
limit?: number;
status?: (string | null);
};
export type ObservabilityReadRunsResponse = (Array<RunRow>);
export type ObservabilityReadEventsData = {
flow?: (string | null);
kind?: 'failure' | 'audit';
limit?: number;
};
export type ObservabilityReadEventsResponse = (Array<EventRow>);
export type ObservabilityReadDeadLettersData = {
limit?: number;
};
export type ObservabilityReadDeadLettersResponse = (Array<DeadLetter>);
export type PrivateCreateUserData = {
requestBody: PrivateUserCreate;
};