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,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;