From eaa0aecccf326e567fa0e2f7a45a051c8c53716e Mon Sep 17 00:00:00 2001 From: Minh Nguyen Date: Mon, 23 Mar 2026 17:49:17 +0700 Subject: [PATCH] fix(ui): scope setup skip state by tenant (#372) Co-authored-by: Minh Nguyen --- ui/web/src/lib/constants.ts | 1 + ui/web/src/lib/setup-skip.ts | 52 +++++++++++++++++++ .../pages/setup/hooks/use-bootstrap-status.ts | 8 ++- ui/web/src/pages/setup/setup-page.tsx | 5 ++ ui/web/src/stores/use-auth-store.ts | 2 + 5 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 ui/web/src/lib/setup-skip.ts diff --git a/ui/web/src/lib/constants.ts b/ui/web/src/lib/constants.ts index e8c02198..7556fff5 100644 --- a/ui/web/src/lib/constants.ts +++ b/ui/web/src/lib/constants.ts @@ -49,6 +49,7 @@ export const LOCAL_STORAGE_KEYS = { SENDER_ID: "goclaw:senderID", TENANT_ID: "goclaw:tenant_id", TENANT_HINT: "goclaw:tenant_hint", + SETUP_SKIPPED: "goclaw:setup_skipped", THEME: "goclaw:theme", SIDEBAR_COLLAPSED: "goclaw:sidebarCollapsed", LANGUAGE: "goclaw:language", diff --git a/ui/web/src/lib/setup-skip.ts b/ui/web/src/lib/setup-skip.ts new file mode 100644 index 00000000..4e465446 --- /dev/null +++ b/ui/web/src/lib/setup-skip.ts @@ -0,0 +1,52 @@ +import { LOCAL_STORAGE_KEYS } from "@/lib/constants"; + +const SETUP_SKIPPED_PREFIX = `${LOCAL_STORAGE_KEYS.SETUP_SKIPPED}:`; + +interface SetupSkipScope { + tenantId?: string; + tenantSlug?: string; + userId?: string; +} + +function getScopePart(value: string | null | undefined, fallback: string) { + const trimmed = value?.trim(); + return trimmed && trimmed.length > 0 ? trimmed : fallback; +} + +function getScopedSetupSkipKey(scope: SetupSkipScope) { + const tenantScope = getScopePart( + scope.tenantId ?? scope.tenantSlug ?? localStorage.getItem(LOCAL_STORAGE_KEYS.TENANT_ID), + "default", + ); + const userScope = getScopePart( + scope.userId ?? localStorage.getItem(LOCAL_STORAGE_KEYS.USER_ID), + "anonymous", + ); + + return `${SETUP_SKIPPED_PREFIX}${encodeURIComponent(tenantScope)}:${encodeURIComponent(userScope)}`; +} + +export function isSetupSkipped(scope: SetupSkipScope) { + if (localStorage.getItem(LOCAL_STORAGE_KEYS.SETUP_SKIPPED) !== null) { + localStorage.removeItem(LOCAL_STORAGE_KEYS.SETUP_SKIPPED); + } + return localStorage.getItem(getScopedSetupSkipKey(scope)) === "1"; +} + +export function markSetupSkipped(scope: SetupSkipScope) { + localStorage.removeItem(LOCAL_STORAGE_KEYS.SETUP_SKIPPED); + localStorage.setItem(getScopedSetupSkipKey(scope), "1"); +} + +export function clearSetupSkippedState() { + const keysToRemove: string[] = []; + for (let i = 0; i < localStorage.length; i += 1) { + const key = localStorage.key(i); + if (!key) continue; + if (key === LOCAL_STORAGE_KEYS.SETUP_SKIPPED || key.startsWith(SETUP_SKIPPED_PREFIX)) { + keysToRemove.push(key); + } + } + + keysToRemove.forEach((key) => localStorage.removeItem(key)); +} diff --git a/ui/web/src/pages/setup/hooks/use-bootstrap-status.ts b/ui/web/src/pages/setup/hooks/use-bootstrap-status.ts index ac8ac1ba..5df4627e 100644 --- a/ui/web/src/pages/setup/hooks/use-bootstrap-status.ts +++ b/ui/web/src/pages/setup/hooks/use-bootstrap-status.ts @@ -2,11 +2,15 @@ import { useMemo } from "react"; import { useProviders } from "@/pages/providers/hooks/use-providers"; import { useAgents } from "@/pages/agents/hooks/use-agents"; import { useAuthStore } from "@/stores/use-auth-store"; +import { isSetupSkipped } from "@/lib/setup-skip"; export type SetupStep = 1 | 2 | 3 | 4 | "complete"; export function useBootstrapStatus() { const connected = useAuthStore((s) => s.connected); + const userId = useAuthStore((s) => s.userId); + const tenantId = useAuthStore((s) => s.tenantId); + const tenantSlug = useAuthStore((s) => s.tenantSlug); const { providers, loading: providersLoading } = useProviders(); const { agents, loading: agentsLoading } = useAgents(); @@ -23,13 +27,13 @@ export function useBootstrapStatus() { const hasAgent = agents.length > 0; // Allow skipping setup entirely via localStorage - const skipped = localStorage.getItem("setup_skipped") === "1"; + const skipped = isSetupSkipped({ userId, tenantId, tenantSlug }); if (skipped) return { needsSetup: false, currentStep: "complete" as SetupStep }; if (!hasProvider) return { needsSetup: true, currentStep: 1 as SetupStep }; if (!hasAgent) return { needsSetup: true, currentStep: 2 as SetupStep }; return { needsSetup: false, currentStep: "complete" as SetupStep }; - }, [loading, providers, agents]); + }, [loading, providers, agents, userId, tenantId, tenantSlug]); return { needsSetup, currentStep, loading, providers, agents }; } diff --git a/ui/web/src/pages/setup/setup-page.tsx b/ui/web/src/pages/setup/setup-page.tsx index 37aa561c..48934542 100644 --- a/ui/web/src/pages/setup/setup-page.tsx +++ b/ui/web/src/pages/setup/setup-page.tsx @@ -11,6 +11,8 @@ import { StepChannel } from "./step-channel"; import { SetupCompleteModal } from "./setup-complete-modal"; import { Building2 } from "lucide-react"; import { ROUTES, SUPPORTED_LANGUAGES, LANGUAGE_LABELS, LOCAL_STORAGE_KEYS } from "@/lib/constants"; +import { markSetupSkipped } from "@/lib/setup-skip"; +import { useAuthStore } from "@/stores/use-auth-store"; import { useUiStore } from "@/stores/use-ui-store"; import { useTenants } from "@/hooks/use-tenants"; import type { ProviderData } from "@/types/provider"; @@ -78,6 +80,8 @@ function TenantSwitcher() { export function SetupPage() { const { t } = useTranslation("setup"); const navigate = useNavigate(); + const userId = useAuthStore((s) => s.userId); + const { currentTenantId, currentTenantSlug } = useTenants(); const { currentStep, loading, providers, agents } = useBootstrapStatus(); const [step, setStep] = useState<1 | 2 | 3 | 4>(1); const [createdProvider, setCreatedProvider] = useState(null); @@ -170,6 +174,7 @@ export function SetupPage() { className="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground transition-colors" onClick={() => { if (window.confirm(t("skipSetupConfirm"))) { + markSetupSkipped({ userId, tenantId: currentTenantId, tenantSlug: currentTenantSlug }); navigate(ROUTES.OVERVIEW, { replace: true }); } }} diff --git a/ui/web/src/stores/use-auth-store.ts b/ui/web/src/stores/use-auth-store.ts index 693a8f01..05a6f88c 100644 --- a/ui/web/src/stores/use-auth-store.ts +++ b/ui/web/src/stores/use-auth-store.ts @@ -1,5 +1,6 @@ import { create } from "zustand"; import { LOCAL_STORAGE_KEYS } from "@/lib/constants"; +import { clearSetupSkippedState } from "@/lib/setup-skip"; import type { TenantMembership } from "@/types/tenant"; type UserRole = "admin" | "operator" | "viewer" | ""; @@ -80,6 +81,7 @@ export const useAuthStore = create((set) => ({ localStorage.removeItem(LOCAL_STORAGE_KEYS.SENDER_ID); localStorage.removeItem(LOCAL_STORAGE_KEYS.TENANT_ID); localStorage.removeItem(LOCAL_STORAGE_KEYS.TENANT_HINT); + clearSetupSkippedState(); set({ token: "", userId: "", senderID: "", connected: false, role: "", serverInfo: null, tenantId: "", tenantName: "", tenantSlug: "", isCrossTenant: false, availableTenants: [],