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

+303 -3
View File
@@ -14,6 +14,9 @@ tags:
- name: Collections
- name: Databases
- name: Telemetry
- name: Integrations
- name: Webhooks
- name: Automation
paths:
/health/liveness:
get:
@@ -1597,6 +1600,162 @@ paths:
$ref: "#/components/schemas/DeployMarker"
"401":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/jobs:
get:
tags: [Automation]
operationId: listScheduledJobs
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
responses:
"200":
description: Scheduled jobs for the project
content:
application/json:
schema:
$ref: "#/components/schemas/ScheduledJobListResponse"
"401":
$ref: "#/components/responses/ErrorResponse"
post:
tags: [Automation]
operationId: createScheduledJob
summary: 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.
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateScheduledJobRequest"
responses:
"201":
description: Job created
content:
application/json:
schema:
$ref: "#/components/schemas/ScheduledJobCreateResponse"
"400":
$ref: "#/components/responses/ErrorResponse"
"401":
$ref: "#/components/responses/ErrorResponse"
"409":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/jobs/{jobID}:
patch:
tags: [Automation]
operationId: updateScheduledJob
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
- $ref: "#/components/parameters/JobID"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateScheduledJobRequest"
responses:
"200":
description: Job updated
content:
application/json:
schema:
$ref: "#/components/schemas/ScheduledJobCreateResponse"
"404":
$ref: "#/components/responses/ErrorResponse"
delete:
tags: [Automation]
operationId: deleteScheduledJob
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
- $ref: "#/components/parameters/JobID"
responses:
"204":
description: Job deleted
"404":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/jobs/{jobID}/runs:
get:
tags: [Automation]
operationId: listScheduledJobRuns
summary: Recent executions, newest first. History is capped at 200 runs per job.
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
- $ref: "#/components/parameters/JobID"
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 200
default: 50
responses:
"200":
description: Job run history
content:
application/json:
schema:
$ref: "#/components/schemas/ScheduledJobRunListResponse"
"404":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/jobs/{jobID}/run:
post:
tags: [Automation]
operationId: runScheduledJob
summary: Triggers an out-of-schedule manual run. Returns the run row in "running" state.
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
- $ref: "#/components/parameters/JobID"
responses:
"202":
description: Manual run queued
content:
application/json:
schema:
$ref: "#/components/schemas/ScheduledJobRun"
"404":
$ref: "#/components/responses/ErrorResponse"
/projects/{projectID}/realtime/stream:
get:
tags: [Automation]
operationId: streamRealtimeEvents
summary: 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.
security:
- bearerAuth: []
- apiKeyAuth: []
parameters:
- $ref: "#/components/parameters/ProjectID"
- name: api_key
in: query
schema:
type: string
- name: token
in: query
schema:
type: string
responses:
"200":
description: text/event-stream of {type, occurred_at, data} messages
"401":
$ref: "#/components/responses/ErrorResponse"
components:
securitySchemes:
bearerAuth:
@@ -1730,6 +1889,13 @@ components:
schema:
type: string
format: uuid
JobID:
name: jobID
in: path
required: true
schema:
type: string
format: uuid
DatabaseQuery:
name: database
in: query
@@ -3106,7 +3272,7 @@ components:
type: array
items:
type: string
description: Event filter (issue.created, deploy.marker, webhook.test). Empty means all events.
description: Event filter (issue.created, deploy.marker, webhook.test, job.run, document.*, object.*). Empty means all events.
enabled:
type: boolean
has_secret:
@@ -3128,7 +3294,7 @@ components:
type: array
items:
type: string
enum: [issue.created, deploy.marker, webhook.test]
enum: [issue.created, deploy.marker, webhook.test, job.run, document.created, document.updated, document.deleted, object.created, object.updated, object.deleted]
enabled:
type: boolean
required: [url]
@@ -3145,7 +3311,7 @@ components:
type: array
items:
type: string
enum: [issue.created, deploy.marker, webhook.test]
enum: [issue.created, deploy.marker, webhook.test, job.run, document.created, document.updated, document.deleted, object.created, object.updated, object.deleted]
enabled:
type: boolean
WebhookCreateResponse:
@@ -3234,3 +3400,137 @@ components:
type: string
format: date-time
required: [recorded_at]
ScheduledJob:
type: object
properties:
id:
type: string
format: uuid
project_id:
type: string
format: uuid
name:
type: string
schedule:
type: string
description: Cron expression (5-field) or descriptor (@hourly, @every 30m).
url:
type: string
payload:
type: object
description: Static JSON payload merged into every delivery's data field.
enabled:
type: boolean
has_secret:
type: boolean
last_run_at:
type: string
format: date-time
nullable: true
last_status:
type: string
enum: [success, failed]
nullable: true
next_run_at:
type: string
format: date-time
nullable: true
created_at:
type: string
format: date-time
required: [id, project_id, name, schedule, url, payload, enabled, has_secret]
CreateScheduledJobRequest:
type: object
properties:
name:
type: string
minLength: 2
schedule:
type: string
description: 'Cron expression, e.g. "*/15 * * * *", or descriptor like "@every 1h".'
url:
type: string
format: uri
secret:
type: string
description: HMAC key for X-Primora-Signature. Generated when omitted.
payload:
type: object
description: Static JSON payload delivered as data on every run.
enabled:
type: boolean
default: true
required: [name, schedule, url]
UpdateScheduledJobRequest:
type: object
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: object
enabled:
type: boolean
ScheduledJobCreateResponse:
allOf:
- $ref: "#/components/schemas/ScheduledJob"
- type: object
properties:
secret:
type: string
description: Plaintext secret — present only when Primora generated it. Store it; it cannot be recovered.
ScheduledJobListResponse:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/ScheduledJob"
required: [items]
ScheduledJobRun:
type: object
properties:
id:
type: string
format: uuid
job_id:
type: string
format: uuid
status:
type: string
enum: [running, success, failed]
triggered_by:
type: string
enum: [schedule, manual]
status_code:
type: integer
nullable: true
error:
type: string
duration_ms:
type: integer
nullable: true
started_at:
type: string
format: date-time
finished_at:
type: string
format: date-time
nullable: true
required: [id, job_id, status, triggered_by]
ScheduledJobRunListResponse:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/ScheduledJobRun"
required: [items]