From 170ac55afd82303140822ead1cde3a716df4b519 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Wed, 4 Feb 2026 22:21:44 +0100 Subject: [PATCH] feat(20-02): wire SSE subscription hooks into dashboard, detail, and inbox pages Add useSubscription hooks to all three UI pages that invalidate React Query caches on domain events: - Dashboard: onTaskUpdate invalidates listInitiatives + listPhases - Detail: onTaskUpdate invalidates phases/tasks/plans, onAgentUpdate invalidates listAgents - Inbox: onAgentUpdate invalidates listWaitingAgents + listMessages Subscription failures are silent (onError: () => {}) so pages degrade gracefully to manual refresh when the backend is not running. --- packages/web/src/routes/inbox.tsx | 12 ++++++++++-- packages/web/src/routes/initiatives/$id.tsx | 17 +++++++++++++++++ packages/web/src/routes/initiatives/index.tsx | 11 +++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/web/src/routes/inbox.tsx b/packages/web/src/routes/inbox.tsx index 23d0f43..3615381 100644 --- a/packages/web/src/routes/inbox.tsx +++ b/packages/web/src/routes/inbox.tsx @@ -13,6 +13,16 @@ export const Route = createFileRoute("/inbox")({ function InboxPage() { const [selectedAgentId, setSelectedAgentId] = useState(null); + // Live updates: invalidate inbox queries on agent events + const utils = trpc.useUtils(); + trpc.onAgentUpdate.useSubscription(undefined, { + onData: () => { + void utils.listWaitingAgents.invalidate(); + void utils.listMessages.invalidate(); + }, + onError: () => {}, + }); + // Data fetching const agentsQuery = trpc.listWaitingAgents.useQuery(); const messagesQuery = trpc.listMessages.useQuery({}); @@ -22,8 +32,6 @@ function InboxPage() { ); // Mutations - const utils = trpc.useUtils(); - const resumeAgentMutation = trpc.resumeAgent.useMutation({ onSuccess: () => { void utils.listWaitingAgents.invalidate(); diff --git a/packages/web/src/routes/initiatives/$id.tsx b/packages/web/src/routes/initiatives/$id.tsx index f2b3392..4e621c1 100644 --- a/packages/web/src/routes/initiatives/$id.tsx +++ b/packages/web/src/routes/initiatives/$id.tsx @@ -209,6 +209,23 @@ function InitiativeDetailPage() { const { id } = Route.useParams(); const navigate = useNavigate(); + // Live updates: invalidate detail queries on task/phase and agent events + const utils = trpc.useUtils(); + trpc.onTaskUpdate.useSubscription(undefined, { + onData: () => { + void utils.listPhases.invalidate(); + void utils.listTasks.invalidate(); + void utils.listPlans.invalidate(); + }, + onError: () => {}, + }); + trpc.onAgentUpdate.useSubscription(undefined, { + onData: () => { + void utils.listAgents.invalidate(); + }, + onError: () => {}, + }); + // State const [selectedTaskId, setSelectedTaskId] = useState(null); const [taskCountsByPhase, setTaskCountsByPhase] = useState< diff --git a/packages/web/src/routes/initiatives/index.tsx b/packages/web/src/routes/initiatives/index.tsx index 69faebb..40a1d3a 100644 --- a/packages/web/src/routes/initiatives/index.tsx +++ b/packages/web/src/routes/initiatives/index.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { Plus } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { trpc } from "@/lib/trpc"; import { InitiativeList } from "@/components/InitiativeList"; import { CreateInitiativeDialog } from "@/components/CreateInitiativeDialog"; @@ -23,6 +24,16 @@ function DashboardPage() { const [statusFilter, setStatusFilter] = useState("all"); const [createDialogOpen, setCreateDialogOpen] = useState(false); + // Live updates: invalidate dashboard queries on task/phase events + const utils = trpc.useUtils(); + trpc.onTaskUpdate.useSubscription(undefined, { + onData: () => { + void utils.listInitiatives.invalidate(); + void utils.listPhases.invalidate(); + }, + onError: () => {}, + }); + return (
{/* Page header */}