Merge pull request #37 from madhura68/feat/sprint-aolrn6ui
Sprint: pbi-55
This commit is contained in:
commit
85a95e5bba
5 changed files with 70 additions and 19 deletions
|
|
@ -3,3 +3,9 @@ DATABASE_URL="postgresql://user:pass@host:5432/dbname"
|
||||||
|
|
||||||
# API token from Scrum4Me → /settings/tokens
|
# API token from Scrum4Me → /settings/tokens
|
||||||
SCRUM4ME_TOKEN=""
|
SCRUM4ME_TOKEN=""
|
||||||
|
|
||||||
|
# Internal push endpoint (main-app) for web-push notifications
|
||||||
|
# Set to the main-app /api/internal/push/send URL; leave empty to disable push (feature-gated).
|
||||||
|
INTERNAL_PUSH_URL="https://scrum4me.example.com/api/internal/push/send"
|
||||||
|
# Shared secret (≥32 chars) — must match INTERNAL_PUSH_SECRET in the main-app env.
|
||||||
|
INTERNAL_PUSH_SECRET="<generate-with: openssl rand -hex 32>"
|
||||||
|
|
|
||||||
|
|
@ -296,6 +296,10 @@ Minimale agent-prompt (geen CLAUDE.md-context nodig):
|
||||||
|
|
||||||
> *Pak de volgende job uit de Scrum4Me-queue.*
|
> *Pak de volgende job uit de Scrum4Me-queue.*
|
||||||
|
|
||||||
|
## Web-push integration
|
||||||
|
|
||||||
|
When `INTERNAL_PUSH_URL` and `INTERNAL_PUSH_SECRET` are set, the MCP server fires a fire-and-forget push notification to the main-app's internal endpoint (`/api/internal/push/send`) on two events: when `ask_user_question` creates a new question (tag `claude-q-<id>`), and when `update_job_status` transitions a job to `done` or `failed` (tag `job-<id>`). Both calls are wrapped in a 5 s `AbortController` timeout and a `try/catch` so a push failure never interrupts the tool response. Omitting the env vars disables the feature entirely. The `INTERNAL_PUSH_SECRET` value must match the one configured in the main-app; generate a fresh secret with `openssl rand -hex 32`.
|
||||||
|
|
||||||
## Schema sync
|
## Schema sync
|
||||||
|
|
||||||
The Prisma schema is the source of truth in the upstream Scrum4Me
|
The Prisma schema is the source of truth in the upstream Scrum4Me
|
||||||
|
|
|
||||||
22
src/lib/push-trigger.ts
Normal file
22
src/lib/push-trigger.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
export type PushPayload = { title: string; body: string; url: string; tag?: string };
|
||||||
|
|
||||||
|
export async function triggerPush(userId: string, payload: PushPayload): Promise<void> {
|
||||||
|
const url = process.env.INTERNAL_PUSH_URL;
|
||||||
|
const secret = process.env.INTERNAL_PUSH_SECRET;
|
||||||
|
if (!url || !secret) return; // feature-gated
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json', authorization: `Bearer ${secret}` },
|
||||||
|
body: JSON.stringify({ userId, payload }),
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
if (!res.ok) console.warn('[push-trigger] non-2xx', res.status);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[push-trigger]', err);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ import { prisma } from '../prisma.js'
|
||||||
import { requireWriteAccess } from '../auth.js'
|
import { requireWriteAccess } from '../auth.js'
|
||||||
import { userCanAccessStory, userOwnsIdea } from '../access.js'
|
import { userCanAccessStory, userOwnsIdea } from '../access.js'
|
||||||
import { toolError, toolJson, withToolErrors } from '../errors.js'
|
import { toolError, toolJson, withToolErrors } from '../errors.js'
|
||||||
|
import { triggerPush } from '../lib/push-trigger.js'
|
||||||
|
|
||||||
const PENDING_TTL_HOURS = 24
|
const PENDING_TTL_HOURS = 24
|
||||||
const POLL_INTERVAL_MS = 2_000
|
const POLL_INTERVAL_MS = 2_000
|
||||||
|
|
@ -127,6 +128,13 @@ export function registerAskUserQuestionTool(server: McpServer) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
void triggerPush(auth.userId, {
|
||||||
|
title: 'Claude heeft een vraag',
|
||||||
|
body: question.slice(0, 120),
|
||||||
|
url: '/notifications',
|
||||||
|
tag: `claude-q-${created.id}`,
|
||||||
|
})
|
||||||
|
|
||||||
// Async-mode (default): return direct.
|
// Async-mode (default): return direct.
|
||||||
if (!wait_seconds || wait_seconds === 0) {
|
if (!wait_seconds || wait_seconds === 0) {
|
||||||
return toolJson(summarize(created))
|
return toolJson(summarize(created))
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { pushBranchForJob } from '../git/push.js'
|
||||||
import { createPullRequest, markPullRequestReady } from '../git/pr.js'
|
import { createPullRequest, markPullRequestReady } from '../git/pr.js'
|
||||||
import { cancelPbiOnFailure } from '../cancel/pbi-cascade.js'
|
import { cancelPbiOnFailure } from '../cancel/pbi-cascade.js'
|
||||||
import { propagateStatusUpwards } from '../lib/tasks-status-update.js'
|
import { propagateStatusUpwards } from '../lib/tasks-status-update.js'
|
||||||
|
import { triggerPush } from '../lib/push-trigger.js'
|
||||||
import { transition as prFlowTransition } from '../flow/pr-flow.js'
|
import { transition as prFlowTransition } from '../flow/pr-flow.js'
|
||||||
import { transition as sprintRunTransition } from '../flow/sprint-run.js'
|
import { transition as sprintRunTransition } from '../flow/sprint-run.js'
|
||||||
import { executeEffects } from '../flow/effects.js'
|
import { executeEffects } from '../flow/effects.js'
|
||||||
|
|
@ -887,30 +888,40 @@ export function registerUpdateJobStatusTool(server: McpServer) {
|
||||||
try {
|
try {
|
||||||
const pg = new Client({ connectionString: process.env.DATABASE_URL })
|
const pg = new Client({ connectionString: process.env.DATABASE_URL })
|
||||||
await pg.connect()
|
await pg.connect()
|
||||||
await pg.query(
|
const notifyPayload: Record<string, unknown> = {
|
||||||
`SELECT pg_notify('scrum4me_changes', $1)`,
|
type: 'claude_job_status',
|
||||||
[
|
job_id: updated.id,
|
||||||
JSON.stringify({
|
user_id: job.user_id,
|
||||||
type: 'claude_job_status',
|
product_id: job.product_id,
|
||||||
job_id: updated.id,
|
status: actualStatus,
|
||||||
task_id: job.task_id,
|
branch: updated.branch ?? undefined,
|
||||||
user_id: job.user_id,
|
pushed_at: updated.pushed_at?.toISOString() ?? undefined,
|
||||||
product_id: job.product_id,
|
pr_url: updated.pr_url ?? undefined,
|
||||||
status: actualStatus,
|
verify_result: updated.verify_result?.toLowerCase() ?? undefined,
|
||||||
branch: updated.branch ?? undefined,
|
summary: updated.summary ?? undefined,
|
||||||
pushed_at: updated.pushed_at?.toISOString() ?? undefined,
|
error: updated.error ?? undefined,
|
||||||
pr_url: updated.pr_url ?? undefined,
|
}
|
||||||
verify_result: updated.verify_result?.toLowerCase() ?? undefined,
|
if (job.task_id) notifyPayload.task_id = job.task_id
|
||||||
summary: updated.summary ?? undefined,
|
if (job.idea_id) {
|
||||||
error: updated.error ?? undefined,
|
notifyPayload.idea_id = job.idea_id
|
||||||
}),
|
notifyPayload.kind = job.kind
|
||||||
],
|
}
|
||||||
)
|
await pg.query(`SELECT pg_notify('scrum4me_changes', $1)`, [JSON.stringify(notifyPayload)])
|
||||||
await pg.end()
|
await pg.end()
|
||||||
} catch {
|
} catch {
|
||||||
// non-fatal — status is already persisted
|
// non-fatal — status is already persisted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actualStatus === 'failed' || actualStatus === 'done') {
|
||||||
|
const isFailed = actualStatus === 'failed'
|
||||||
|
void triggerPush(job.user_id, {
|
||||||
|
title: isFailed ? 'Job gefaald' : 'Job klaar',
|
||||||
|
body: (updated.summary ?? updated.error ?? `Job ${updated.id}`).slice(0, 120),
|
||||||
|
url: updated.pr_url ?? '/dashboard',
|
||||||
|
tag: `job-${updated.id}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Best-effort worktree cleanup on terminal transitions (skip if push failed — worktree preserved)
|
// Best-effort worktree cleanup on terminal transitions (skip if push failed — worktree preserved)
|
||||||
if (
|
if (
|
||||||
(actualStatus === 'done' || actualStatus === 'failed' || actualStatus === 'skipped') &&
|
(actualStatus === 'done' || actualStatus === 'failed' || actualStatus === 'skipped') &&
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue