fix(web): wire Refresh Groups button to syncGroupsAction with live SSE refresh

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 <form action={async() => 'use server'}> placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yiekheng 2026-05-10 19:13:24 +08:00
parent 40d788302c
commit 5c48e0e85f
2 changed files with 70 additions and 8 deletions

View File

@ -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) {
</Badge>
</div>
{/* Refresh button — no-op placeholder, wired in Task 17 */}
<form action={async () => { "use server"; /* wired in Task 17 */ }}>
<Button type="submit" variant="outline" size="sm" className="shrink-0">
<RefreshCwIcon />
Refresh Groups
</Button>
</form>
<RefreshGroupsClient accountId={account.id} />
</div>
{/* Search */}

View File

@ -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 (
<Button
type="button"
variant="outline"
size="sm"
className="shrink-0"
disabled={busy}
onClick={trigger}
>
{busy ? (
<Loader2Icon className="size-4 animate-spin" />
) : (
<RefreshCwIcon className="size-4" />
)}
{busy ? "Syncing…" : "Refresh Groups"}
</Button>
);
}