Skip to content

Commit b014a02

Browse files
committed
fix: coalesce transient channel restart status
1 parent 7cd04b6 commit b014a02

1 file changed

Lines changed: 55 additions & 8 deletions

File tree

apps/web/src/pages/home.tsx

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ type BudgetBannerDebugMode = "actual" | Exclude<BudgetBannerStatus, "healthy">;
8383

8484
const budgetBannerDebugStorageKey = "nexu_budget_banner_debug_mode";
8585
const showBudgetBannerDebugPanel = import.meta.env.DEV;
86+
const STARTUP_GRACE_MS = 15_000;
87+
const GATEWAY_RESTART_COALESCE_MS = 10_000;
88+
const GATEWAY_HEALTHY_POLLS_TO_SETTLE = 2;
8689

8790
function formatRelativeTime(
8891
date: string | null | undefined,
@@ -373,12 +376,15 @@ export function HomePage() {
373376
const videoRef = useRef<HTMLVideoElement>(null);
374377
const [videoHover, setVideoHover] = useState(false);
375378
const [pendingChannelId, setPendingChannelId] = useState<string | null>(null);
379+
const [pendingGatewayRestartUntil, setPendingGatewayRestartUntil] = useState<
380+
number | null
381+
>(null);
376382
const connectingToastIdRef = useRef<string | number | null>(null);
377383
const previousLiveStatusesRef = useRef<Record<string, ChannelLiveStatus>>({});
384+
const stableGatewayPollsRef = useRef(0);
378385
// Suppress status-change toasts during startup grace period (first config
379386
// push triggers SIGUSR1 → brief disconnect → reconnect cycle).
380387
const mountedAtRef = useRef(Date.now());
381-
const STARTUP_GRACE_MS = 15_000;
382388

383389
const CHANNEL_OPTIONS = useMemo(() => getChannelOptions(t), [t]);
384390

@@ -596,10 +602,25 @@ export function HomePage() {
596602
return new Map(entries.map((entry) => [entry.channelId, entry]));
597603
}, [liveStatus]);
598604

605+
const isPendingGatewayRestartCoalesced = useMemo(() => {
606+
return (
607+
pendingChannelId !== null &&
608+
pendingGatewayRestartUntil !== null &&
609+
Date.now() < pendingGatewayRestartUntil
610+
);
611+
}, [pendingChannelId, pendingGatewayRestartUntil]);
612+
599613
const agentIndicator = useMemo(() => {
600614
if (!hasOperationalContext || !liveStatus?.agent) {
601615
return null;
602616
}
617+
if (isPendingGatewayRestartCoalesced) {
618+
return {
619+
colorClass: "bg-[var(--color-warning)]",
620+
pulse: true,
621+
label: t("home.agent.starting"),
622+
};
623+
}
603624
return liveStatus.agent.alive
604625
? {
605626
colorClass: "bg-[var(--color-success)]",
@@ -611,7 +632,7 @@ export function HomePage() {
611632
pulse: true,
612633
label: t("home.agent.starting"),
613634
};
614-
}, [hasOperationalContext, liveStatus, t]);
635+
}, [hasOperationalContext, isPendingGatewayRestartCoalesced, liveStatus, t]);
615636
const budgetBannerDebugPanel = showBudgetBannerDebugPanel ? (
616637
<BudgetBannerDebugPanel
617638
actualStatus="healthy"
@@ -638,6 +659,8 @@ export function HomePage() {
638659
const handleChannelCreated = useCallback(
639660
(channelId: string) => {
640661
setPendingChannelId(channelId);
662+
setPendingGatewayRestartUntil(Date.now() + GATEWAY_RESTART_COALESCE_MS);
663+
stableGatewayPollsRef.current = 0;
641664
connectingToastIdRef.current = toast.loading(
642665
t("home.channel.phase.connecting"),
643666
);
@@ -656,26 +679,47 @@ export function HomePage() {
656679
toast.loading(t("home.channel.phase.configuring"), { id: toastId });
657680
return;
658681
}
659-
if (pending.status === "connected") {
682+
const gatewayHealthy =
683+
liveStatus?.gatewayConnected === true && liveStatus?.agent.alive === true;
684+
if (pending.status === "connected" && gatewayHealthy) {
685+
stableGatewayPollsRef.current += 1;
686+
const shouldSettleNow =
687+
!isPendingGatewayRestartCoalesced ||
688+
stableGatewayPollsRef.current >= GATEWAY_HEALTHY_POLLS_TO_SETTLE;
689+
if (!shouldSettleNow) {
690+
toast.loading(t("home.channel.phase.configuring"), { id: toastId });
691+
return;
692+
}
660693
toast.success(t("home.channel.phase.done"), { id: toastId });
661694
connectingToastIdRef.current = null;
662695
setPendingChannelId(null);
696+
setPendingGatewayRestartUntil(null);
697+
stableGatewayPollsRef.current = 0;
663698
return;
664699
}
700+
stableGatewayPollsRef.current = 0;
665701
if (pending.status === "error") {
666702
toast.error(pending.lastError ?? t("home.channel.error"), {
667703
id: toastId,
668704
});
669705
connectingToastIdRef.current = null;
670706
setPendingChannelId(null);
707+
setPendingGatewayRestartUntil(null);
671708
return;
672709
}
673-
if (pending.status === "restarting") {
710+
if (pending.status === "restarting" || isPendingGatewayRestartCoalesced) {
674711
toast.loading(t("home.channel.phase.configuring"), { id: toastId });
675712
return;
676713
}
677714
toast.loading(t("home.channel.phase.almostReady"), { id: toastId });
678-
}, [liveStatusByChannelId, pendingChannelId, t]);
715+
}, [
716+
isPendingGatewayRestartCoalesced,
717+
liveStatus?.agent.alive,
718+
liveStatus?.gatewayConnected,
719+
liveStatusByChannelId,
720+
pendingChannelId,
721+
t,
722+
]);
679723

680724
useEffect(() => {
681725
const previous = previousLiveStatusesRef.current;
@@ -1063,9 +1107,12 @@ export function HomePage() {
10631107
const isPendingChannel =
10641108
actionableChannelId === pendingChannelId;
10651109
const effectiveStatus: ChannelLiveStatus | undefined =
1066-
isPendingChannel &&
1067-
(!statusEntry || statusEntry.status === "disconnected")
1068-
? "connecting"
1110+
isPendingChannel && pendingGatewayRestartUntil !== null
1111+
? isPendingGatewayRestartCoalesced
1112+
? "restarting"
1113+
: !statusEntry || statusEntry.status === "disconnected"
1114+
? "connecting"
1115+
: statusEntry.status
10691116
: statusEntry?.status;
10701117
const statusMeta = getChannelStatusMeta(
10711118
effectiveStatus,

0 commit comments

Comments
 (0)