From 5c48e0e85f711e4ed616b6ce213ad0f8d08d784f Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sun, 10 May 2026 19:13:24 +0800 Subject: [PATCH] fix(web): wire Refresh Groups button to syncGroupsAction with live SSE refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The button was a placeholder that submitted to a no-op server action, so clicking did nothing. Replace with a small client component that: 1. Calls syncGroupsAction(accountId) to pgNotify the bot. 2. Listens for the bot's groups.synced event over SSE and router.refresh()es when it arrives so the new rows appear without a manual reload. 3. Disables the button + shows a Syncing… label while the sync is in flight, with a 15s safety timeout if the bot or SSE channel drops so the spinner doesn't strand. Drop the in-place
'use server'}> placeholder. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../web/src/app/accounts/[id]/groups/page.tsx | 10 +-- .../[id]/groups/refresh-groups-client.tsx | 68 +++++++++++++++++++ 2 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 apps/web/src/app/accounts/[id]/groups/refresh-groups-client.tsx diff --git a/apps/web/src/app/accounts/[id]/groups/page.tsx b/apps/web/src/app/accounts/[id]/groups/page.tsx index 4661589..05e165f 100644 --- a/apps/web/src/app/accounts/[id]/groups/page.tsx +++ b/apps/web/src/app/accounts/[id]/groups/page.tsx @@ -4,7 +4,6 @@ import { ArrowLeftIcon, SearchIcon, UsersIcon, - RefreshCwIcon, Users2Icon, } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -16,6 +15,7 @@ import { } from "@/components/ui/card"; import { getSeededOperator } from "@/lib/operator"; import { listGroupsForAccount } from "@/lib/queries"; +import { RefreshGroupsClient } from "./refresh-groups-client"; interface Props { params: Promise<{ id: string }>; @@ -57,13 +57,7 @@ export default async function GroupsListPage({ params, searchParams }: Props) { - {/* Refresh button — no-op placeholder, wired in Task 17 */} - { "use server"; /* wired in Task 17 */ }}> - - + {/* Search */} diff --git a/apps/web/src/app/accounts/[id]/groups/refresh-groups-client.tsx b/apps/web/src/app/accounts/[id]/groups/refresh-groups-client.tsx new file mode 100644 index 0000000..0933063 --- /dev/null +++ b/apps/web/src/app/accounts/[id]/groups/refresh-groups-client.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { Loader2Icon, RefreshCwIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useEvents } from "@/hooks/use-events"; +import { syncGroupsAction } from "@/actions/accounts"; + +interface RefreshGroupsClientProps { + accountId: string; +} + +/** + * Two-stage refresh button: + * 1. Click → server action pgNotifies the bot to start a sync. + * 2. Bot finishes → emits `groups.synced` over SSE → router.refresh() + * re-fetches the page so the new rows appear without the operator + * having to reload manually. + * + * The button stays in its "syncing" state until either the + * `groups.synced` event arrives for this account or 15 s pass (so a + * disconnected bot doesn't strand the spinner forever). + */ +export function RefreshGroupsClient({ accountId }: RefreshGroupsClientProps) { + const router = useRouter(); + const [pending, start] = useTransition(); + const [waiting, setWaiting] = useState(false); + + useEvents({ + "groups.synced": (data) => { + if (data.accountId !== accountId) return; + setWaiting(false); + router.refresh(); + }, + }); + + function trigger() { + start(async () => { + const fd = new FormData(); + fd.append("accountId", accountId); + await syncGroupsAction(fd); + setWaiting(true); + // Belt-and-braces: if the bot is unreachable or the SSE channel + // drops, drop the spinner after 15 s instead of leaving it stuck. + window.setTimeout(() => setWaiting(false), 15_000); + }); + } + + const busy = pending || waiting; + return ( + + ); +}