From ebef093d3f0ed03e62c5b2a52444d878d8e1ef05 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Fri, 6 Mar 2026 13:25:31 +0100 Subject: [PATCH] fix: Add missing event routing for initiative status real-time refresh 7 of 12 initiative activity state transitions were broken due to missing event routing at three layers: SSE event arrays, live-update prefix rules, and mutation invalidation map. - Add initiative:changes_requested to ALL_EVENT_TYPES and TASK_EVENT_TYPES - Add initiative:/agent: prefix rules to initiatives list and detail pages - Add approveInitiativeReview, requestInitiativeChanges, requestPhaseChanges to INVALIDATION_MAP; add listInitiatives to approvePhase - Extract INITIATIVE_LIST_RULES constant for reuse --- apps/server/trpc/subscriptions.ts | 2 ++ apps/web/src/hooks/index.ts | 3 ++- apps/web/src/hooks/useLiveUpdates.ts | 11 +++++++++++ apps/web/src/lib/invalidation.ts | 5 ++++- apps/web/src/routes/initiatives/$id.tsx | 3 ++- apps/web/src/routes/initiatives/index.tsx | 7 ++----- 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/apps/server/trpc/subscriptions.ts b/apps/server/trpc/subscriptions.ts index 027e055..b4102bd 100644 --- a/apps/server/trpc/subscriptions.ts +++ b/apps/server/trpc/subscriptions.ts @@ -70,6 +70,7 @@ export const ALL_EVENT_TYPES: DomainEventType[] = [ 'chat:session_closed', 'initiative:pending_review', 'initiative:review_approved', + 'initiative:changes_requested', ]; /** @@ -102,6 +103,7 @@ export const TASK_EVENT_TYPES: DomainEventType[] = [ 'phase:merged', 'initiative:pending_review', 'initiative:review_approved', + 'initiative:changes_requested', ]; /** diff --git a/apps/web/src/hooks/index.ts b/apps/web/src/hooks/index.ts index 0211b7a..a33ef3c 100644 --- a/apps/web/src/hooks/index.ts +++ b/apps/web/src/hooks/index.ts @@ -7,7 +7,8 @@ export { useAutoSave } from './useAutoSave.js'; export { useDebounce, useDebounceWithImmediate } from './useDebounce.js'; -export { useLiveUpdates } from './useLiveUpdates.js'; +export { useLiveUpdates, INITIATIVE_LIST_RULES } from './useLiveUpdates.js'; +export type { LiveUpdateRule } from './useLiveUpdates.js'; export { useRefineAgent } from './useRefineAgent.js'; export { useConflictAgent } from './useConflictAgent.js'; export { useSubscriptionWithErrorHandling } from './useSubscriptionWithErrorHandling.js'; diff --git a/apps/web/src/hooks/useLiveUpdates.ts b/apps/web/src/hooks/useLiveUpdates.ts index 6179619..5ab36f1 100644 --- a/apps/web/src/hooks/useLiveUpdates.ts +++ b/apps/web/src/hooks/useLiveUpdates.ts @@ -15,6 +15,17 @@ export interface LiveUpdateRule { * * Encapsulates error toast + reconnect config so pages don't duplicate boilerplate. */ +/** + * Reusable rules for any page displaying initiative cards. + * Covers all event prefixes that can change derived initiative activity state. + */ +export const INITIATIVE_LIST_RULES: LiveUpdateRule[] = [ + { prefix: 'initiative:', invalidate: ['listInitiatives'] }, + { prefix: 'task:', invalidate: ['listInitiatives'] }, + { prefix: 'phase:', invalidate: ['listInitiatives'] }, + { prefix: 'agent:', invalidate: ['listInitiatives'] }, +]; + export function useLiveUpdates(rules: LiveUpdateRule[]) { const utils = trpc.useUtils(); diff --git a/apps/web/src/lib/invalidation.ts b/apps/web/src/lib/invalidation.ts index eb5b517..ae38d45 100644 --- a/apps/web/src/lib/invalidation.ts +++ b/apps/web/src/lib/invalidation.ts @@ -49,12 +49,15 @@ const INVALIDATION_MAP: Partial> = { createInitiative: ["listInitiatives"], updateInitiative: ["listInitiatives", "getInitiative"], updateInitiativeProjects: ["getInitiative"], + approveInitiativeReview: ["listInitiatives", "getInitiative"], + requestInitiativeChanges: ["listInitiatives", "getInitiative"], // --- Phases --- createPhase: ["listPhases", "listInitiativePhaseDependencies"], deletePhase: ["listPhases", "listInitiativeTasks", "listInitiativePhaseDependencies", "listChangeSets"], updatePhase: ["listPhases", "getPhase"], - approvePhase: ["listPhases", "listInitiativeTasks"], + approvePhase: ["listPhases", "listInitiativeTasks", "listInitiatives"], + requestPhaseChanges: ["listPhases", "listInitiativeTasks", "listPhaseTasks", "getInitiative"], queuePhase: ["listPhases"], createPhaseDependency: ["getPhaseDependencies", "listInitiativePhaseDependencies", "listPhaseTaskDependencies"], removePhaseDependency: ["getPhaseDependencies", "listInitiativePhaseDependencies", "listPhaseTaskDependencies"], diff --git a/apps/web/src/routes/initiatives/$id.tsx b/apps/web/src/routes/initiatives/$id.tsx index f56dbed..e62de70 100644 --- a/apps/web/src/routes/initiatives/$id.tsx +++ b/apps/web/src/routes/initiatives/$id.tsx @@ -12,7 +12,7 @@ import { ExecutionTab } from "@/components/ExecutionTab"; import { ReviewTab } from "@/components/review"; import { PipelineTab } from "@/components/pipeline"; import { useLiveUpdates } from "@/hooks"; -import type { LiveUpdateRule } from "@/hooks/useLiveUpdates"; +import type { LiveUpdateRule } from "@/hooks"; type Tab = "content" | "plan" | "execution" | "review"; const TABS: Tab[] = ["content", "plan", "execution", "review"]; @@ -31,6 +31,7 @@ function InitiativeDetailPage() { // Single SSE stream for all live updates — memoized to avoid re-subscribe on render const liveUpdateRules = useMemo(() => [ + { prefix: 'initiative:', invalidate: ['getInitiative'] }, { prefix: 'task:', invalidate: ['listPhases', 'listTasks', 'listInitiativeTasks', 'getPhaseDependencies', 'listPhaseTaskDependencies'] }, { prefix: 'phase:', invalidate: ['listPhases', 'listTasks', 'listInitiativePhaseDependencies', 'getPhaseDependencies'] }, { prefix: 'agent:', invalidate: ['listAgents', 'getActiveRefineAgent'] }, diff --git a/apps/web/src/routes/initiatives/index.tsx b/apps/web/src/routes/initiatives/index.tsx index 5407dd1..140b7c3 100644 --- a/apps/web/src/routes/initiatives/index.tsx +++ b/apps/web/src/routes/initiatives/index.tsx @@ -5,7 +5,7 @@ import { Plus } from "lucide-react"; import { Button } from "@/components/ui/button"; import { InitiativeList } from "@/components/InitiativeList"; import { CreateInitiativeDialog } from "@/components/CreateInitiativeDialog"; -import { useLiveUpdates } from "@/hooks"; +import { useLiveUpdates, INITIATIVE_LIST_RULES } from "@/hooks"; import { trpc } from "@/lib/trpc"; export const Route = createFileRoute("/initiatives/")({ @@ -29,10 +29,7 @@ function DashboardPage() { const projectsQuery = trpc.listProjects.useQuery(); // Single SSE stream for live updates - useLiveUpdates([ - { prefix: 'task:', invalidate: ['listInitiatives'] }, - { prefix: 'phase:', invalidate: ['listInitiatives'] }, - ]); + useLiveUpdates(INITIATIVE_LIST_RULES); return (