mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 07:41:23 +00:00
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:
1 parent
4b7fdf35ad
commit
9e9a3d5f98
49 files changed
+3555
-32
No files matched your search
+149
-1
@@ -3,6 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
AutomationService,
|
||||
CreateBucketRequest,
|
||||
IntegrationsService,
|
||||
OpenAPI,
|
||||
@@ -574,7 +575,20 @@ server.registerTool(
|
||||
|
||||
const webhookRef = z.string().describe("Webhook id (see primora_list_webhooks)");
|
||||
const webhookEvents = z
|
||||
.array(z.enum(["issue.created", "deploy.marker", "webhook.test"]))
|
||||
.array(
|
||||
z.enum([
|
||||
"issue.created",
|
||||
"deploy.marker",
|
||||
"webhook.test",
|
||||
"job.run",
|
||||
"document.created",
|
||||
"document.updated",
|
||||
"document.deleted",
|
||||
"object.created",
|
||||
"object.updated",
|
||||
"object.deleted",
|
||||
]),
|
||||
)
|
||||
.describe("Event filter — empty means every event");
|
||||
|
||||
server.registerTool(
|
||||
@@ -732,4 +746,138 @@ server.registerTool(
|
||||
},
|
||||
);
|
||||
|
||||
/* ---------------------------------------------------------- */
|
||||
/* automation — scheduled jobs */
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
const jobRef = z.string().describe("Scheduled job id (see primora_list_jobs)");
|
||||
|
||||
server.registerTool(
|
||||
"primora_list_jobs",
|
||||
{
|
||||
description: "List scheduled jobs for a project — cron schedule, target URL, last/next run",
|
||||
inputSchema: { projectId: z.string().optional() },
|
||||
},
|
||||
async ({ projectId: p }) => {
|
||||
try {
|
||||
return ok(await AutomationService.listScheduledJobs({ projectId: projectId(p) }));
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"primora_create_job",
|
||||
{
|
||||
description:
|
||||
"Create a scheduled job — cron expression or @descriptor, signed POST to url. Omit secret to generate one (returned once)",
|
||||
inputSchema: {
|
||||
name: z.string(),
|
||||
schedule: z
|
||||
.string()
|
||||
.describe('Cron (5-field, e.g. "*/15 * * * *") or descriptor: @hourly, @daily, @every 30m'),
|
||||
url: z.string().describe("HTTPS endpoint (http allowed only for private/self-hosted targets)"),
|
||||
secret: z.string().optional(),
|
||||
payload: z.record(z.unknown()).optional().describe("Static JSON payload merged into every delivery"),
|
||||
enabled: z.boolean().optional(),
|
||||
projectId: z.string().optional(),
|
||||
},
|
||||
},
|
||||
async ({ name, schedule, url, secret, payload, enabled, projectId: p }) => {
|
||||
try {
|
||||
return ok(
|
||||
await AutomationService.createScheduledJob({
|
||||
projectId: projectId(p),
|
||||
requestBody: { name, schedule, url, secret, payload, enabled },
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"primora_update_job",
|
||||
{
|
||||
description: "Update a scheduled job — name, schedule, url, payload, enabled flag, or rotate the secret",
|
||||
inputSchema: {
|
||||
jobId: jobRef,
|
||||
name: z.string().optional(),
|
||||
schedule: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
secret: z.string().optional(),
|
||||
payload: z.record(z.unknown()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
projectId: z.string().optional(),
|
||||
},
|
||||
},
|
||||
async ({ jobId, name, schedule, url, secret, payload, enabled, projectId: p }) => {
|
||||
try {
|
||||
return ok(
|
||||
await AutomationService.updateScheduledJob({
|
||||
projectId: projectId(p),
|
||||
jobId,
|
||||
requestBody: { name, schedule, url, secret, payload, enabled },
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"primora_delete_job",
|
||||
{
|
||||
description: "Delete a scheduled job and stop future runs",
|
||||
inputSchema: { jobId: jobRef, projectId: z.string().optional() },
|
||||
},
|
||||
async ({ jobId, projectId: p }) => {
|
||||
try {
|
||||
await AutomationService.deleteScheduledJob({ projectId: projectId(p), jobId });
|
||||
return ok({ deleted: jobId });
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"primora_list_job_runs",
|
||||
{
|
||||
description: "Run history for a scheduled job — status, HTTP code, duration, errors",
|
||||
inputSchema: {
|
||||
jobId: jobRef,
|
||||
limit: z.number().int().optional(),
|
||||
projectId: z.string().optional(),
|
||||
},
|
||||
},
|
||||
async ({ jobId, limit, projectId: p }) => {
|
||||
try {
|
||||
return ok(
|
||||
await AutomationService.listScheduledJobRuns({ projectId: projectId(p), jobId, limit }),
|
||||
);
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"primora_run_job",
|
||||
{
|
||||
description: "Trigger a manual run of a scheduled job now",
|
||||
inputSchema: { jobId: jobRef, projectId: z.string().optional() },
|
||||
},
|
||||
async ({ jobId, projectId: p }) => {
|
||||
try {
|
||||
return ok(await AutomationService.runScheduledJob({ projectId: projectId(p), jobId }));
|
||||
} catch (e) {
|
||||
return fail(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await server.connect(new StdioServerTransport());
|
||||
Reference in new issue
Block a user