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

+20 -7
View File
@@ -28,15 +28,28 @@ import (
)
const (
WebhookEventIssueCreated = "issue.created"
WebhookEventDeployMarker = "deploy.marker"
WebhookEventTest = "webhook.test"
WebhookEventIssueCreated = "issue.created"
WebhookEventDeployMarker = "deploy.marker"
WebhookEventTest = "webhook.test"
WebhookEventDocumentCreated = "document.created"
WebhookEventDocumentUpdated = "document.updated"
WebhookEventDocumentDeleted = "document.deleted"
WebhookEventObjectCreated = "object.created"
WebhookEventObjectUpdated = "object.updated"
WebhookEventObjectDeleted = "object.deleted"
)
var webhookEventTypes = map[string]bool{
WebhookEventIssueCreated: true,
WebhookEventDeployMarker: true,
WebhookEventTest: true,
WebhookEventIssueCreated: true,
WebhookEventDeployMarker: true,
WebhookEventTest: true,
WebhookEventJobRun: true,
WebhookEventDocumentCreated: true,
WebhookEventDocumentUpdated: true,
WebhookEventDocumentDeleted: true,
WebhookEventObjectCreated: true,
WebhookEventObjectUpdated: true,
WebhookEventObjectDeleted: true,
}
const webhookMaxAttempts = 3
@@ -436,7 +449,7 @@ func (s *PlatformService) CreateDeployMarker(ctx context.Context, actor *models.
if _, err := s.repo.Queries().CreateAuditLog(ctx, newAuditParams(project.OrganizationID, projectID, actor, requestID, "deploy.marker", "deploy_marker", marker.Ref+marker.Version, meta)); err != nil {
return DeployMarker{}, err
}
s.dispatchEvent(ctx, projectID, WebhookEventDeployMarker, map[string]any{
s.publishEvent(ctx, projectID, WebhookEventDeployMarker, map[string]any{
"version": marker.Version,
"ref": marker.Ref,
"environment": marker.Environment,