feat(ST-qcmqlv2i): SSE + idea-store — user_question events verwerken

- IdeaUserQuestion type + UserQuestionEvent type in idea-store
- userQuestions state + initUserQuestions + handleUserQuestionEvent
- clearForIdea filtert ook userQuestions voor het idea
- IdeaJobKind uitgebreid met PLAN_CHAT
- notifications/route.ts: UserQuestionPayload, isUserQuestionPayload,
  user_question events forwarden naar SSE, userQuestionsInit in state-event
- use-notifications-realtime.ts: UserQuestionPayload handler +
  initUserQuestions aanroepen bij state-event

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scrum4Me Agent 2026-05-05 17:37:57 +02:00
parent 226bd05594
commit c0e8270e5f
3 changed files with 106 additions and 9 deletions

View file

@ -49,23 +49,37 @@ interface IdeaJobPayload {
idea_id: string
user_id: string
product_id?: string | null
kind: 'IDEA_GRILL' | 'IDEA_MAKE_PLAN'
kind: 'IDEA_GRILL' | 'IDEA_MAKE_PLAN' | 'PLAN_CHAT'
status: string
}
type NotifyPayload = QuestionPayload | IdeaJobPayload
// UserQuestion-payloads: emitted by app/api/user-questions/[id]/answer and
// actions/user-questions.ts via prisma.$executeRaw pg_notify.
interface UserQuestionPayload {
op: 'I' | 'U'
entity: 'user_question'
id: string
idea_id: string
status: 'pending' | 'answered'
}
type NotifyPayload = QuestionPayload | IdeaJobPayload | UserQuestionPayload
function isQuestionPayload(p: NotifyPayload): p is QuestionPayload {
return 'entity' in p && p.entity === 'question'
}
function isUserQuestionPayload(p: NotifyPayload): p is UserQuestionPayload {
return 'entity' in p && p.entity === 'user_question'
}
function isIdeaJobPayload(p: NotifyPayload): p is IdeaJobPayload {
return (
'type' in p &&
(p.type === 'claude_job_enqueued' || p.type === 'claude_job_status') &&
'idea_id' in p &&
'kind' in p &&
(p.kind === 'IDEA_GRILL' || p.kind === 'IDEA_MAKE_PLAN')
(p.kind === 'IDEA_GRILL' || p.kind === 'IDEA_MAKE_PLAN' || p.kind === 'PLAN_CHAT')
)
}
@ -164,6 +178,13 @@ export async function GET(request: NextRequest) {
return
}
// UserQuestion (PLAN_CHAT answer-event): user-scoped via idea ownership.
if (isUserQuestionPayload(payload)) {
if (!accessibleIdeaIds.has(payload.idea_id)) return
enqueue(`data: ${msg.payload}\n\n`)
return
}
if (!isQuestionPayload(payload)) return
// Idea-question: alleen voor de eigenaar van het idee.
@ -264,7 +285,14 @@ export async function GET(request: NextRequest) {
}),
].sort((a, b) => (a.created_at < b.created_at ? 1 : -1))
enqueue(`event: state\ndata: ${JSON.stringify({ questions: stateQuestions })}\n\n`)
const userQuestionsInit = await prisma.userQuestion.findMany({
where: { idea: { user_id: userId } },
orderBy: { created_at: 'desc' },
take: 100,
select: { id: true, idea_id: true, question: true, answer: true, status: true, created_at: true },
})
enqueue(`event: state\ndata: ${JSON.stringify({ questions: stateQuestions, userQuestions: userQuestionsInit.map(uq => ({ ...uq, created_at: uq.created_at.toISOString() })) })}\n\n`)
heartbeatTimer = setInterval(() => {
enqueue(`: heartbeat\n\n`)