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
This commit is contained in:
Lukas May
2026-03-06 13:25:31 +01:00
parent 52e238924c
commit ebef093d3f
6 changed files with 23 additions and 8 deletions

View File

@@ -70,6 +70,7 @@ export const ALL_EVENT_TYPES: DomainEventType[] = [
'chat:session_closed', 'chat:session_closed',
'initiative:pending_review', 'initiative:pending_review',
'initiative:review_approved', 'initiative:review_approved',
'initiative:changes_requested',
]; ];
/** /**
@@ -102,6 +103,7 @@ export const TASK_EVENT_TYPES: DomainEventType[] = [
'phase:merged', 'phase:merged',
'initiative:pending_review', 'initiative:pending_review',
'initiative:review_approved', 'initiative:review_approved',
'initiative:changes_requested',
]; ];
/** /**

View File

@@ -7,7 +7,8 @@
export { useAutoSave } from './useAutoSave.js'; export { useAutoSave } from './useAutoSave.js';
export { useDebounce, useDebounceWithImmediate } from './useDebounce.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 { useRefineAgent } from './useRefineAgent.js';
export { useConflictAgent } from './useConflictAgent.js'; export { useConflictAgent } from './useConflictAgent.js';
export { useSubscriptionWithErrorHandling } from './useSubscriptionWithErrorHandling.js'; export { useSubscriptionWithErrorHandling } from './useSubscriptionWithErrorHandling.js';

View File

@@ -15,6 +15,17 @@ export interface LiveUpdateRule {
* *
* Encapsulates error toast + reconnect config so pages don't duplicate boilerplate. * 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[]) { export function useLiveUpdates(rules: LiveUpdateRule[]) {
const utils = trpc.useUtils(); const utils = trpc.useUtils();

View File

@@ -49,12 +49,15 @@ const INVALIDATION_MAP: Partial<Record<MutationName, QueryName[]>> = {
createInitiative: ["listInitiatives"], createInitiative: ["listInitiatives"],
updateInitiative: ["listInitiatives", "getInitiative"], updateInitiative: ["listInitiatives", "getInitiative"],
updateInitiativeProjects: ["getInitiative"], updateInitiativeProjects: ["getInitiative"],
approveInitiativeReview: ["listInitiatives", "getInitiative"],
requestInitiativeChanges: ["listInitiatives", "getInitiative"],
// --- Phases --- // --- Phases ---
createPhase: ["listPhases", "listInitiativePhaseDependencies"], createPhase: ["listPhases", "listInitiativePhaseDependencies"],
deletePhase: ["listPhases", "listInitiativeTasks", "listInitiativePhaseDependencies", "listChangeSets"], deletePhase: ["listPhases", "listInitiativeTasks", "listInitiativePhaseDependencies", "listChangeSets"],
updatePhase: ["listPhases", "getPhase"], updatePhase: ["listPhases", "getPhase"],
approvePhase: ["listPhases", "listInitiativeTasks"], approvePhase: ["listPhases", "listInitiativeTasks", "listInitiatives"],
requestPhaseChanges: ["listPhases", "listInitiativeTasks", "listPhaseTasks", "getInitiative"],
queuePhase: ["listPhases"], queuePhase: ["listPhases"],
createPhaseDependency: ["getPhaseDependencies", "listInitiativePhaseDependencies", "listPhaseTaskDependencies"], createPhaseDependency: ["getPhaseDependencies", "listInitiativePhaseDependencies", "listPhaseTaskDependencies"],
removePhaseDependency: ["getPhaseDependencies", "listInitiativePhaseDependencies", "listPhaseTaskDependencies"], removePhaseDependency: ["getPhaseDependencies", "listInitiativePhaseDependencies", "listPhaseTaskDependencies"],

View File

@@ -12,7 +12,7 @@ import { ExecutionTab } from "@/components/ExecutionTab";
import { ReviewTab } from "@/components/review"; import { ReviewTab } from "@/components/review";
import { PipelineTab } from "@/components/pipeline"; import { PipelineTab } from "@/components/pipeline";
import { useLiveUpdates } from "@/hooks"; import { useLiveUpdates } from "@/hooks";
import type { LiveUpdateRule } from "@/hooks/useLiveUpdates"; import type { LiveUpdateRule } from "@/hooks";
type Tab = "content" | "plan" | "execution" | "review"; type Tab = "content" | "plan" | "execution" | "review";
const TABS: 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 // Single SSE stream for all live updates — memoized to avoid re-subscribe on render
const liveUpdateRules = useMemo<LiveUpdateRule[]>(() => [ const liveUpdateRules = useMemo<LiveUpdateRule[]>(() => [
{ prefix: 'initiative:', invalidate: ['getInitiative'] },
{ prefix: 'task:', invalidate: ['listPhases', 'listTasks', 'listInitiativeTasks', 'getPhaseDependencies', 'listPhaseTaskDependencies'] }, { prefix: 'task:', invalidate: ['listPhases', 'listTasks', 'listInitiativeTasks', 'getPhaseDependencies', 'listPhaseTaskDependencies'] },
{ prefix: 'phase:', invalidate: ['listPhases', 'listTasks', 'listInitiativePhaseDependencies', 'getPhaseDependencies'] }, { prefix: 'phase:', invalidate: ['listPhases', 'listTasks', 'listInitiativePhaseDependencies', 'getPhaseDependencies'] },
{ prefix: 'agent:', invalidate: ['listAgents', 'getActiveRefineAgent'] }, { prefix: 'agent:', invalidate: ['listAgents', 'getActiveRefineAgent'] },

View File

@@ -5,7 +5,7 @@ import { Plus } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { InitiativeList } from "@/components/InitiativeList"; import { InitiativeList } from "@/components/InitiativeList";
import { CreateInitiativeDialog } from "@/components/CreateInitiativeDialog"; import { CreateInitiativeDialog } from "@/components/CreateInitiativeDialog";
import { useLiveUpdates } from "@/hooks"; import { useLiveUpdates, INITIATIVE_LIST_RULES } from "@/hooks";
import { trpc } from "@/lib/trpc"; import { trpc } from "@/lib/trpc";
export const Route = createFileRoute("/initiatives/")({ export const Route = createFileRoute("/initiatives/")({
@@ -29,10 +29,7 @@ function DashboardPage() {
const projectsQuery = trpc.listProjects.useQuery(); const projectsQuery = trpc.listProjects.useQuery();
// Single SSE stream for live updates // Single SSE stream for live updates
useLiveUpdates([ useLiveUpdates(INITIATIVE_LIST_RULES);
{ prefix: 'task:', invalidate: ['listInitiatives'] },
{ prefix: 'phase:', invalidate: ['listInitiatives'] },
]);
return ( return (
<motion.div <motion.div