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
|
||||
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.*
|
||||
|
||||
## 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
|
||||
|
||||
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 { userCanAccessStory, userOwnsIdea } from '../access.js'
|
||||
import { toolError, toolJson, withToolErrors } from '../errors.js'
|
||||
import { triggerPush } from '../lib/push-trigger.js'
|
||||
|
||||
const PENDING_TTL_HOURS = 24
|
||||
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.
|
||||
if (!wait_seconds || wait_seconds === 0) {
|
||||
return toolJson(summarize(created))
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { pushBranchForJob } from '../git/push.js'
|
|||
import { createPullRequest, markPullRequestReady } from '../git/pr.js'
|
||||
import { cancelPbiOnFailure } from '../cancel/pbi-cascade.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 sprintRunTransition } from '../flow/sprint-run.js'
|
||||
import { executeEffects } from '../flow/effects.js'
|
||||
|
|
@ -887,30 +888,40 @@ export function registerUpdateJobStatusTool(server: McpServer) {
|
|||
try {
|
||||
const pg = new Client({ connectionString: process.env.DATABASE_URL })
|
||||
await pg.connect()
|
||||
await pg.query(
|
||||
`SELECT pg_notify('scrum4me_changes', $1)`,
|
||||
[
|
||||
JSON.stringify({
|
||||
type: 'claude_job_status',
|
||||
job_id: updated.id,
|
||||
task_id: job.task_id,
|
||||
user_id: job.user_id,
|
||||
product_id: job.product_id,
|
||||
status: actualStatus,
|
||||
branch: updated.branch ?? undefined,
|
||||
pushed_at: updated.pushed_at?.toISOString() ?? undefined,
|
||||
pr_url: updated.pr_url ?? undefined,
|
||||
verify_result: updated.verify_result?.toLowerCase() ?? undefined,
|
||||
summary: updated.summary ?? undefined,
|
||||
error: updated.error ?? undefined,
|
||||
}),
|
||||
],
|
||||
)
|
||||
const notifyPayload: Record<string, unknown> = {
|
||||
type: 'claude_job_status',
|
||||
job_id: updated.id,
|
||||
user_id: job.user_id,
|
||||
product_id: job.product_id,
|
||||
status: actualStatus,
|
||||
branch: updated.branch ?? undefined,
|
||||
pushed_at: updated.pushed_at?.toISOString() ?? undefined,
|
||||
pr_url: updated.pr_url ?? undefined,
|
||||
verify_result: updated.verify_result?.toLowerCase() ?? undefined,
|
||||
summary: updated.summary ?? undefined,
|
||||
error: updated.error ?? undefined,
|
||||
}
|
||||
if (job.task_id) notifyPayload.task_id = job.task_id
|
||||
if (job.idea_id) {
|
||||
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()
|
||||
} catch {
|
||||
// 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)
|
||||
if (
|
||||
(actualStatus === 'done' || actualStatus === 'failed' || actualStatus === 'skipped') &&
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue