feat: phase 7 automation & realtime — scheduled jobs, domain events, SSE

Scheduled jobs (migration 00009): cron expressions + @every descriptors,
in-process scheduler (15s scan, missed-run collapse), signed HTTP POST
delivery (X-Primora-Signature), AES-256-GCM sealed secrets returned once,
run history with status/duration/http-code, manual execution, retention
pruning. Viewer reads, admin/developer writes.

Domain events: publishEvent fans document/object/issue/deploy/job
mutations to realtime subscribers and matching webhooks through one
path. Webhook event filter extended to job.run, document.*, object.*.

Realtime: GET /projects/:id/realtime/stream SSE with 25s keepalives and
?token=/?api_key= auth for EventSource clients.

Automation dashboard page: Schedules tab (CRUD, run history, manual run,
secret-once banner) + Live events tab (live badge, pause/clear). Demo
mode parity. primora jobs:* CLI group, six primora_*_job* MCP tools,
AutomationService in the generated client.

Fixes folded in: gzip middleware buffered SSE flushes (broke EventSource
on both streams — */stream now bypasses compression); event payloads
double-marshaled json.RawMessage as base64; compose hardcoded localhost
trusted origins so non-default ports broke sign-in; frontend auth base
URL baked an absolute localhost instead of resolving /auth on origin.

Verified live: stack up on :8085, signed delivery HMAC-checked at sink,
@every 30s schedule firing, run history rendering, document.created
landing in the live events feed.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-18 15:15:01 +02:00
1 parent 4b7fdf35ad
commit 9e9a3d5f98
49 files changed
+3555 -32

No files matched your search

@@ -38,6 +38,7 @@ export { CreateIntegrationRequest } from './models/CreateIntegrationRequest';
export { CreateInvitationRequest } from './models/CreateInvitationRequest';
export type { CreateOrganizationRequest } from './models/CreateOrganizationRequest';
export type { CreateProjectRequest } from './models/CreateProjectRequest';
export type { CreateScheduledJobRequest } from './models/CreateScheduledJobRequest';
export type { CreateWebhookRequest } from './models/CreateWebhookRequest';
export type { DatabaseQuery } from './models/DatabaseQuery';
export type { DBConnection } from './models/DBConnection';
@@ -75,6 +76,7 @@ export type { IntegrationListResponse } from './models/IntegrationListResponse';
export type { IntegrationTestResult } from './models/IntegrationTestResult';
export type { Invitation } from './models/Invitation';
export type { InvitationID } from './models/InvitationID';
export type { JobID } from './models/JobID';
export type { MeResponse } from './models/MeResponse';
export type { MetricPoint } from './models/MetricPoint';
export type { MetricSeriesResponse } from './models/MetricSeriesResponse';
@@ -95,6 +97,11 @@ export type { ProjectSummary } from './models/ProjectSummary';
export type { ReadinessStatus } from './models/ReadinessStatus';
export type { RybbitMetricItem } from './models/RybbitMetricItem';
export type { RybbitSite } from './models/RybbitSite';
export { ScheduledJob } from './models/ScheduledJob';
export type { ScheduledJobCreateResponse } from './models/ScheduledJobCreateResponse';
export type { ScheduledJobListResponse } from './models/ScheduledJobListResponse';
export { ScheduledJobRun } from './models/ScheduledJobRun';
export type { ScheduledJobRunListResponse } from './models/ScheduledJobRunListResponse';
export type { SchemaQuery } from './models/SchemaQuery';
export type { TableQuery } from './models/TableQuery';
export type { TelemetryComponent } from './models/TelemetryComponent';
@@ -112,6 +119,7 @@ export { UpdateOrganizationMemberRoleRequest } from './models/UpdateOrganization
export type { UpdateOrganizationRequest } from './models/UpdateOrganizationRequest';
export { UpdateProjectMemberRoleRequest } from './models/UpdateProjectMemberRoleRequest';
export type { UpdateProjectRequest } from './models/UpdateProjectRequest';
export type { UpdateScheduledJobRequest } from './models/UpdateScheduledJobRequest';
export type { UpdateWebhookRequest } from './models/UpdateWebhookRequest';
export type { UserID } from './models/UserID';
export type { UserSummary } from './models/UserSummary';
@@ -153,6 +161,7 @@ export { $CreateIntegrationRequest } from './schemas/$CreateIntegrationRequest';
export { $CreateInvitationRequest } from './schemas/$CreateInvitationRequest';
export { $CreateOrganizationRequest } from './schemas/$CreateOrganizationRequest';
export { $CreateProjectRequest } from './schemas/$CreateProjectRequest';
export { $CreateScheduledJobRequest } from './schemas/$CreateScheduledJobRequest';
export { $CreateWebhookRequest } from './schemas/$CreateWebhookRequest';
export { $DatabaseQuery } from './schemas/$DatabaseQuery';
export { $DBConnection } from './schemas/$DBConnection';
@@ -190,6 +199,7 @@ export { $IntegrationListResponse } from './schemas/$IntegrationListResponse';
export { $IntegrationTestResult } from './schemas/$IntegrationTestResult';
export { $Invitation } from './schemas/$Invitation';
export { $InvitationID } from './schemas/$InvitationID';
export { $JobID } from './schemas/$JobID';
export { $MeResponse } from './schemas/$MeResponse';
export { $MetricPoint } from './schemas/$MetricPoint';
export { $MetricSeriesResponse } from './schemas/$MetricSeriesResponse';
@@ -210,6 +220,11 @@ export { $ProjectSummary } from './schemas/$ProjectSummary';
export { $ReadinessStatus } from './schemas/$ReadinessStatus';
export { $RybbitMetricItem } from './schemas/$RybbitMetricItem';
export { $RybbitSite } from './schemas/$RybbitSite';
export { $ScheduledJob } from './schemas/$ScheduledJob';
export { $ScheduledJobCreateResponse } from './schemas/$ScheduledJobCreateResponse';
export { $ScheduledJobListResponse } from './schemas/$ScheduledJobListResponse';
export { $ScheduledJobRun } from './schemas/$ScheduledJobRun';
export { $ScheduledJobRunListResponse } from './schemas/$ScheduledJobRunListResponse';
export { $SchemaQuery } from './schemas/$SchemaQuery';
export { $TableQuery } from './schemas/$TableQuery';
export { $TelemetryComponent } from './schemas/$TelemetryComponent';
@@ -227,6 +242,7 @@ export { $UpdateOrganizationMemberRoleRequest } from './schemas/$UpdateOrganizat
export { $UpdateOrganizationRequest } from './schemas/$UpdateOrganizationRequest';
export { $UpdateProjectMemberRoleRequest } from './schemas/$UpdateProjectMemberRoleRequest';
export { $UpdateProjectRequest } from './schemas/$UpdateProjectRequest';
export { $UpdateScheduledJobRequest } from './schemas/$UpdateScheduledJobRequest';
export { $UpdateWebhookRequest } from './schemas/$UpdateWebhookRequest';
export { $UserID } from './schemas/$UserID';
export { $UserSummary } from './schemas/$UserSummary';
@@ -237,6 +253,7 @@ export { $WebhookDeliveryListResponse } from './schemas/$WebhookDeliveryListResp
export { $WebhookID } from './schemas/$WebhookID';
export { $WebhookListResponse } from './schemas/$WebhookListResponse';
export { AutomationService } from './services/AutomationService';
export { CollectionsService } from './services/CollectionsService';
export { DatabasesService } from './services/DatabasesService';
export { HealthService } from './services/HealthService';
@@ -0,0 +1,22 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateScheduledJobRequest = {
name: string;
/**
* Cron expression, e.g. "*15 * * * *", or descriptor like "@every 1h".
*/
schedule: string;
url: string;
/**
* HMAC key for X-Primora-Signature. Generated when omitted.
*/
secret?: string;
/**
* Static JSON payload delivered as data on every run.
*/
payload?: Record<string, any>;
enabled?: boolean;
};
@@ -8,7 +8,7 @@ export type CreateWebhookRequest = {
* HMAC key for X-Primora-Signature. Generated when omitted.
*/
secret?: string;
events?: Array<'issue.created' | 'deploy.marker' | 'webhook.test'>;
events?: Array<'issue.created' | 'deploy.marker' | 'webhook.test' | 'job.run' | 'document.created' | 'document.updated' | 'document.deleted' | 'object.created' | 'object.updated' | 'object.deleted'>;
enabled?: boolean;
};
@@ -0,0 +1,5 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type JobID = string;
@@ -0,0 +1,31 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ScheduledJob = {
id: string;
project_id: string;
name: string;
/**
* Cron expression (5-field) or descriptor (@hourly, @every 30m).
*/
schedule: string;
url: string;
/**
* Static JSON payload merged into every delivery's data field.
*/
payload: Record<string, any>;
enabled: boolean;
has_secret: boolean;
last_run_at?: string | null;
last_status?: ScheduledJob.last_status | null;
next_run_at?: string | null;
created_at?: string;
};
export namespace ScheduledJob {
export enum last_status {
SUCCESS = 'success',
FAILED = 'failed',
}
}
@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ScheduledJob } from './ScheduledJob';
export type ScheduledJobCreateResponse = (ScheduledJob & {
/**
* Plaintext secret — present only when Primora generated it. Store it; it cannot be recovered.
*/
secret?: string;
});
@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ScheduledJob } from './ScheduledJob';
export type ScheduledJobListResponse = {
items: Array<ScheduledJob>;
};
@@ -0,0 +1,27 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ScheduledJobRun = {
id: string;
job_id: string;
status: ScheduledJobRun.status;
triggered_by: ScheduledJobRun.triggered_by;
status_code?: number | null;
error?: string;
duration_ms?: number | null;
started_at?: string;
finished_at?: string | null;
};
export namespace ScheduledJobRun {
export enum status {
RUNNING = 'running',
SUCCESS = 'success',
FAILED = 'failed',
}
export enum triggered_by {
SCHEDULE = 'schedule',
MANUAL = 'manual',
}
}
@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ScheduledJobRun } from './ScheduledJobRun';
export type ScheduledJobRunListResponse = {
items: Array<ScheduledJobRun>;
};
@@ -0,0 +1,16 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UpdateScheduledJobRequest = {
name?: string;
schedule?: string;
url?: string;
/**
* Replace the signing secret. Omit to keep the current one.
*/
secret?: string;
payload?: Record<string, any>;
enabled?: boolean;
};
@@ -8,7 +8,7 @@ export type UpdateWebhookRequest = {
* Replace the signing secret. Omit to keep the current one.
*/
secret?: string;
events?: Array<'issue.created' | 'deploy.marker' | 'webhook.test'>;
events?: Array<'issue.created' | 'deploy.marker' | 'webhook.test' | 'job.run' | 'document.created' | 'document.updated' | 'document.deleted' | 'object.created' | 'object.updated' | 'object.deleted'>;
enabled?: boolean;
};
@@ -7,7 +7,7 @@ export type Webhook = {
project_id: string;
url: string;
/**
* Event filter (issue.created, deploy.marker, webhook.test). Empty means all events.
* Event filter (issue.created, deploy.marker, webhook.test, job.run, document.*, object.*). Empty means all events.
*/
events: Array<string>;
enabled: boolean;
@@ -0,0 +1,37 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $CreateScheduledJobRequest = {
properties: {
name: {
type: 'string',
isRequired: true,
minLength: 2,
},
schedule: {
type: 'string',
description: `Cron expression, e.g. "*/15 * * * *", or descriptor like "@every 1h".`,
isRequired: true,
},
url: {
type: 'string',
isRequired: true,
format: 'uri',
},
secret: {
type: 'string',
description: `HMAC key for X-Primora-Signature. Generated when omitted.`,
},
payload: {
type: 'dictionary',
contains: {
properties: {
},
},
},
enabled: {
type: 'boolean',
},
},
} as const;
@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $JobID = {
type: 'string',
format: 'uuid',
} as const;
@@ -0,0 +1,65 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $ScheduledJob = {
properties: {
id: {
type: 'string',
isRequired: true,
format: 'uuid',
},
project_id: {
type: 'string',
isRequired: true,
format: 'uuid',
},
name: {
type: 'string',
isRequired: true,
},
schedule: {
type: 'string',
description: `Cron expression (5-field) or descriptor (@hourly, @every 30m).`,
isRequired: true,
},
url: {
type: 'string',
isRequired: true,
},
payload: {
type: 'dictionary',
contains: {
properties: {
},
},
isRequired: true,
},
enabled: {
type: 'boolean',
isRequired: true,
},
has_secret: {
type: 'boolean',
isRequired: true,
},
last_run_at: {
type: 'string',
isNullable: true,
format: 'date-time',
},
last_status: {
type: 'Enum',
isNullable: true,
},
next_run_at: {
type: 'string',
isNullable: true,
format: 'date-time',
},
created_at: {
type: 'string',
format: 'date-time',
},
},
} as const;
@@ -0,0 +1,17 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $ScheduledJobCreateResponse = {
type: 'all-of',
contains: [{
type: 'ScheduledJob',
}, {
properties: {
secret: {
type: 'string',
description: `Plaintext secret — present only when Primora generated it. Store it; it cannot be recovered.`,
},
},
}],
} as const;
@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $ScheduledJobListResponse = {
properties: {
items: {
type: 'array',
contains: {
type: 'ScheduledJob',
},
isRequired: true,
},
},
} as const;
@@ -0,0 +1,46 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $ScheduledJobRun = {
properties: {
id: {
type: 'string',
isRequired: true,
format: 'uuid',
},
job_id: {
type: 'string',
isRequired: true,
format: 'uuid',
},
status: {
type: 'Enum',
isRequired: true,
},
triggered_by: {
type: 'Enum',
isRequired: true,
},
status_code: {
type: 'number',
isNullable: true,
},
error: {
type: 'string',
},
duration_ms: {
type: 'number',
isNullable: true,
},
started_at: {
type: 'string',
format: 'date-time',
},
finished_at: {
type: 'string',
isNullable: true,
format: 'date-time',
},
},
} as const;
@@ -0,0 +1,15 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $ScheduledJobRunListResponse = {
properties: {
items: {
type: 'array',
contains: {
type: 'ScheduledJobRun',
},
isRequired: true,
},
},
} as const;
@@ -0,0 +1,33 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export const $UpdateScheduledJobRequest = {
properties: {
name: {
type: 'string',
minLength: 2,
},
schedule: {
type: 'string',
},
url: {
type: 'string',
format: 'uri',
},
secret: {
type: 'string',
description: `Replace the signing secret. Omit to keep the current one.`,
},
payload: {
type: 'dictionary',
contains: {
properties: {
},
},
},
enabled: {
type: 'boolean',
},
},
} as const;
@@ -0,0 +1,194 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { CreateScheduledJobRequest } from '../models/CreateScheduledJobRequest';
import type { ScheduledJobCreateResponse } from '../models/ScheduledJobCreateResponse';
import type { ScheduledJobListResponse } from '../models/ScheduledJobListResponse';
import type { ScheduledJobRun } from '../models/ScheduledJobRun';
import type { ScheduledJobRunListResponse } from '../models/ScheduledJobRunListResponse';
import type { UpdateScheduledJobRequest } from '../models/UpdateScheduledJobRequest';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class AutomationService {
/**
* @returns ScheduledJobListResponse Scheduled jobs for the project
* @throws ApiError
*/
public static listScheduledJobs({
projectId,
}: {
projectId: string,
}): CancelablePromise<ScheduledJobListResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/projects/{projectID}/jobs',
path: {
'projectID': projectId,
},
errors: {
401: `Error response`,
},
});
}
/**
* Runs a signed POST to the job URL on a cron schedule (standard 5-field cron or @every/@hourly descriptors). Payload is signed X-Primora-Signature like webhook deliveries; an empty secret generates one returned once. HTTPS required for public targets, HTTP allowed for private/self-hosted.
* @returns ScheduledJobCreateResponse Job created
* @throws ApiError
*/
public static createScheduledJob({
projectId,
requestBody,
}: {
projectId: string,
requestBody: CreateScheduledJobRequest,
}): CancelablePromise<ScheduledJobCreateResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/projects/{projectID}/jobs',
path: {
'projectID': projectId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
400: `Error response`,
401: `Error response`,
409: `Error response`,
},
});
}
/**
* @returns ScheduledJobCreateResponse Job updated
* @throws ApiError
*/
public static updateScheduledJob({
projectId,
jobId,
requestBody,
}: {
projectId: string,
jobId: string,
requestBody: UpdateScheduledJobRequest,
}): CancelablePromise<ScheduledJobCreateResponse> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/projects/{projectID}/jobs/{jobID}',
path: {
'projectID': projectId,
'jobID': jobId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
404: `Error response`,
},
});
}
/**
* @returns void
* @throws ApiError
*/
public static deleteScheduledJob({
projectId,
jobId,
}: {
projectId: string,
jobId: string,
}): CancelablePromise<void> {
return __request(OpenAPI, {
method: 'DELETE',
url: '/projects/{projectID}/jobs/{jobID}',
path: {
'projectID': projectId,
'jobID': jobId,
},
errors: {
404: `Error response`,
},
});
}
/**
* Recent executions, newest first. History is capped at 200 runs per job.
* @returns ScheduledJobRunListResponse Job run history
* @throws ApiError
*/
public static listScheduledJobRuns({
projectId,
jobId,
limit = 50,
}: {
projectId: string,
jobId: string,
limit?: number,
}): CancelablePromise<ScheduledJobRunListResponse> {
return __request(OpenAPI, {
method: 'GET',
url: '/projects/{projectID}/jobs/{jobID}/runs',
path: {
'projectID': projectId,
'jobID': jobId,
},
query: {
'limit': limit,
},
errors: {
404: `Error response`,
},
});
}
/**
* Triggers an out-of-schedule manual run. Returns the run row in "running" state.
* @returns ScheduledJobRun Manual run queued
* @throws ApiError
*/
public static runScheduledJob({
projectId,
jobId,
}: {
projectId: string,
jobId: string,
}): CancelablePromise<ScheduledJobRun> {
return __request(OpenAPI, {
method: 'POST',
url: '/projects/{projectID}/jobs/{jobID}/run',
path: {
'projectID': projectId,
'jobID': jobId,
},
errors: {
404: `Error response`,
},
});
}
/**
* Server-sent events stream of project domain events — document/object mutations, new issues, deploy markers, job runs. EventSource clients pass `?api_key=` or `?token=` since headers cannot be set.
* @returns any text/event-stream of {type, occurred_at, data} messages
* @throws ApiError
*/
public static streamRealtimeEvents({
projectId,
apiKey,
token,
}: {
projectId: string,
apiKey?: string,
token?: string,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/projects/{projectID}/realtime/stream',
path: {
'projectID': projectId,
},
query: {
'api_key': apiKey,
'token': token,
},
errors: {
401: `Error response`,
},
});
}
}