fix(ui): scope setup skip state by tenant (#372)

Co-authored-by: Minh Nguyen <gbut.277@gmail.com>
This commit is contained in:
Minh NguyenandMinh Nguyen authored and GitHub committed 2026-03-23 17:49:17 +07:00
1 parent 4868f6c4d8
commit eaa0aecccf
5 files changed
+66 -2

No files matched your search

+1
View File
@@ -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",
+52
View File
@@ -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));
}
@@ -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 };
}
+5
View File
@@ -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<ProviderData | null>(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 });
}
}}
+2
View File
@@ -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<AuthState>((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: [],