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

@@ -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`,
},
});
}
}