From b088705b6128a8a247692e9937bad4e7f5cc2d4c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 15 Sep 2026 17:16:25 +0000 Subject: [PATCH] Skip the server setup wizard on desktop first launch. Desktop (OCTOP_DESKTOP) now binds local SQLite, provisions the guest session, and opens on the model / API-key step with a skip-to-workspace path. Self-hosted installs keep the password, database, and admin wizard. --- CHANGELOG.md | 2 + dashboard/src/api/modules/auth.ts | 8 + dashboard/src/api/request.setup.test.ts | 37 ++++ dashboard/src/api/request.ts | 22 ++- dashboard/src/components/AuthGuard.test.tsx | 45 +++++ dashboard/src/components/AuthGuard.tsx | 53 +++++- dashboard/src/locales/en.json | 3 + dashboard/src/locales/zh.json | 3 + dashboard/src/pages/Login/index.test.tsx | 32 ++++ dashboard/src/pages/Login/index.tsx | 11 +- dashboard/src/pages/Setup/index.test.tsx | 144 +++++++++++++++ dashboard/src/pages/Setup/index.tsx | 164 +++++++++++++----- dashboard/src/pages/Setup/steps/ModelStep.tsx | 14 +- dashboard/src/utils/desktopOnboarding.test.ts | 25 +++ dashboard/src/utils/desktopOnboarding.ts | 30 ++++ src/octop/api/openapi_meta.py | 4 +- src/octop/api/routers/setup.py | 7 + src/octop/infra/server.py | 23 ++- tests/integration/test_local_session.py | 26 +++ tests/integration/test_server_lifecycle.py | 17 ++ tests/integration/test_setup_wizard.py | 2 + 21 files changed, 611 insertions(+), 61 deletions(-) create mode 100644 dashboard/src/pages/Setup/index.test.tsx create mode 100644 dashboard/src/utils/desktopOnboarding.test.ts create mode 100644 dashboard/src/utils/desktopOnboarding.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bd6f089..8cf85b6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,14 @@ ### 变更 +- 桌面端(`OCTOP_DESKTOP` / `FREEOS_DESKTOP`)首次打开不再经过验证启动密码、选择数据库、创建管理员三步:自动绑定本地 SQLite 并使用已有 guest / local-session。首屏只保留配置 LLM API Key,可「跳过,进入工作台」,之后仍可在设置里配模型。自托管服务端向导不变。 - 用户可见吉祥物从章鱼换成 XYAI 白色机器人姿势包(欢迎 / 空态 / 探头 / 思考 / 打字 / 任务);桌面启动页与 openXYOS 首页使用同一角色,不再加载章鱼 WebM。 - 桌面更新通道只跟踪 `github.com/XYAIStudio/FreeOS/releases`。不再读取 PyPI `octop`、腾讯云 COS 或其它 Octop 镜像;当前已是最新 FreeOS `v0.0.1` 时不会误报更新。 - 默认主题与残留 Octop `rose` 存储一次性迁移为 XYAI 蓝 `#0033FF`(之后仍可手动选玫瑰粉)。 ### 修复 +- 桌面 / 本地会话不再被 `setup_required` 短路:`GET /setup/status` 之后仍可调用 `POST /auth/local-session`,不会误跳进服务端密码向导。 - Windows 打开时不再因 WebView2 `Navigate`/`SetURL` 在 Chromium 未就绪时触发 Go panic。主窗口等 WebView 点火后再跳转;`SetURL` 失败会重试并显示 FreeOS 提示,而不是原始堆栈。设置窗口延后创建,避免两个 WebView2 同时初始化同一用户目录。用户数据目录必须可写,并设置 `WEBVIEW2_USER_DATA_FOLDER`。 - 已安装的 FreeOS 不再继承残留的 `~/.octop` 作为桌面家目录。无 `FREEOS_STAMP` 的 `~/.octop/portable` 会被丢弃;健康检查要求 `product=freeos`,避免连上旧 Octop 进程后出现红标登录墙。桌面端 `?desktop=1` 会记住本地会话并持续重试,不再落到登录页。 - Windows NSIS 安装完成后双击桌面 / 开始菜单快捷方式(或 `FreeOS.exe`)能打开窗口:快捷方式工作目录固定为 `$INSTDIR`,WebView2 用户数据写到 `%LOCALAPPDATA%\FreeOS\WebView2`(不再落到 Program Files),结束页用未提权 token 启动以免把 `~/.freeos` 标成 High integrity,并忽略非 `FreeOS.exe` 的残留 `desktop.pid`。 diff --git a/dashboard/src/api/modules/auth.ts b/dashboard/src/api/modules/auth.ts index afdd4643..a6e646aa 100644 --- a/dashboard/src/api/modules/auth.ts +++ b/dashboard/src/api/modules/auth.ts @@ -36,6 +36,10 @@ export interface AuthStatus { database_bound: boolean; /** Active control-plane driver when bound (`sqlite` | `postgresql`). */ database_driver?: string | null; + /** True when the host process is the Wails desktop / portable shell. */ + desktop?: boolean; + /** True when at least one LLM provider is already configured. */ + has_providers?: boolean; } export interface OctopUser { @@ -77,6 +81,8 @@ interface RawSetupStatus { wizard_password_path?: string; database_bound?: boolean; database_driver?: string | null; + desktop?: boolean; + has_providers?: boolean; } interface RawLoginResponse { @@ -121,6 +127,8 @@ export const authApi = { wizard_password_path: raw.wizard_password_path, database_bound: raw.database_bound ?? false, database_driver: raw.database_driver ?? null, + desktop: raw.desktop ?? false, + has_providers: raw.has_providers ?? false, }; return applySetupFlags(value); } finally { diff --git a/dashboard/src/api/request.setup.test.ts b/dashboard/src/api/request.setup.test.ts index 2507f1b8..904f14c6 100644 --- a/dashboard/src/api/request.setup.test.ts +++ b/dashboard/src/api/request.setup.test.ts @@ -62,6 +62,43 @@ describe("setup lockdown handling", () => { expect(replace).not.toHaveBeenCalled(); }); + it("still allows /auth/local-session while lockdown is known", async () => { + mod.markSetupRequired(); + fetchMock.mockResolvedValueOnce( + new Response( + JSON.stringify({ + access_token: "guest-token", + token_type: "Bearer", + expires_in: 3600, + user: { id: 1, username: "local", role: "admin" }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ), + ); + + const body = await mod.request<{ access_token: string }>( + "/auth/local-session", + { method: "POST" }, + ); + expect(body.access_token).toBe("guest-token"); + expect(fetchMock).toHaveBeenCalledOnce(); + expect(replace).not.toHaveBeenCalled(); + }); + + it("keeps desktop=1 when redirecting the Wails shell to setup", async () => { + Object.defineProperty(window, "location", { + configurable: true, + value: { pathname: "/chat", replace }, + }); + sessionStorage.setItem("freeos:desktop-shell", "1"); + + await expect(mod.request("/agents")).rejects.toThrow( + mod.SetupRequiredError, + ); + expect(replace).toHaveBeenCalledWith("/setup?desktop=1"); + sessionStorage.clear(); + }); + it("still allows /setup/status while lockdown is known", async () => { mod.markSetupRequired(); fetchMock.mockResolvedValueOnce( diff --git a/dashboard/src/api/request.ts b/dashboard/src/api/request.ts index 55634e9e..9d8ef80a 100644 --- a/dashboard/src/api/request.ts +++ b/dashboard/src/api/request.ts @@ -89,12 +89,28 @@ function handleSetupRequired(): void { } _redirectingToSetup = true; // Full reload drops any in-flight React state. - window.location.replace("/setup"); + window.location.replace(setupRedirectPath()); } -/** Wizard endpoints stay reachable while lockdown is active. */ +/** Remembered Wails shell flag — keep ``?desktop=1`` if the query was dropped. */ +function setupRedirectPath(): string { + try { + if (window.sessionStorage.getItem("freeos:desktop-shell") === "1") { + return "/setup?desktop=1"; + } + } catch { + /* private mode */ + } + return "/setup"; +} + +/** Wizard + desktop guest session stay reachable while lockdown is active. */ function isSetupApiPath(path: string): boolean { - return path === "/setup/status" || path.startsWith("/setup/"); + return ( + path === "/setup/status" || + path.startsWith("/setup/") || + path === "/auth/local-session" + ); } /** diff --git a/dashboard/src/components/AuthGuard.test.tsx b/dashboard/src/components/AuthGuard.test.tsx index 9b4a3397..48ff17b5 100644 --- a/dashboard/src/components/AuthGuard.test.tsx +++ b/dashboard/src/components/AuthGuard.test.tsx @@ -47,6 +47,7 @@ function renderGuard() { describe("AuthGuard local session", () => { beforeEach(() => { localStorage.clear(); + sessionStorage.clear(); localSession.mockReset(); getAuthStatus.mockReset(); me.mockReset(); @@ -87,6 +88,50 @@ describe("AuthGuard local session", () => { expect(screen.queryByText("usable app")).toBeNull(); }); + it("sends the desktop first launch to model setup, not the login wall", async () => { + getAuthStatus.mockResolvedValue({ + setup_required: true, + has_providers: false, + desktop: true, + }); + localSession.mockResolvedValue({ + access_token: "guest-token", + token_type: "Bearer", + expires_in: 3600, + user: { + id: 1, + username: "local", + role: "admin", + display_name: "FreeOS", + locale: "zh", + is_local: true, + }, + token: "guest-token", + }); + + render( + + + +
usable app
+ + } + /> + login wall} /> + model setup} /> +
+
, + ); + + expect(await screen.findByText("model setup")).toBeInTheDocument(); + expect(screen.queryByText("login wall")).toBeNull(); + expect(screen.queryByText("usable app")).toBeNull(); + expect(getAuthToken()).toBe("guest-token"); + }); + it("never opens the login wall inside the desktop shell", async () => { getAuthStatus.mockResolvedValue({ setup_required: false }); localSession.mockRejectedValue(new Error("interactive login required")); diff --git a/dashboard/src/components/AuthGuard.tsx b/dashboard/src/components/AuthGuard.tsx index 40365907..9e18a4e1 100644 --- a/dashboard/src/components/AuthGuard.tsx +++ b/dashboard/src/components/AuthGuard.tsx @@ -4,6 +4,7 @@ import { Spin } from "antd"; import { clearAuthToken, getAuthToken, setAuthToken } from "../api/request"; import { authApi, type OctopUser } from "../api/modules/auth"; import { applyUserLocale } from "../utils/locale"; +import { desktopPostSessionPath } from "../utils/desktopOnboarding"; import { isDesktopShell } from "../utils/desktopShell"; import { CurrentUserProvider } from "../hooks/useCurrentUser"; import { AuthPromptProvider } from "../context/AuthPromptContext"; @@ -38,20 +39,32 @@ export default function AuthGuard({ children }: AuthGuardProps) { } }; - const adoptLocal = async (): Promise => { + const enterAfterSession = async (me: OctopUser, hasProviders: boolean) => { + if (desktop && desktopPostSessionPath(hasProviders) === "/setup") { + if (!cancelled) navigate("/setup", { replace: true }); + return; + } + await adopt(me); + }; + + const adoptLocal = async (hasProviders = false): Promise => { try { const res = await authApi.localSession(); setAuthToken(res.access_token); - await adopt(res.user); + await enterAfterSession(res.user, hasProviders); return true; } catch { return false; } }; - const tryLocalSession = async (attempts: number, delayMs: number) => { + const tryLocalSession = async ( + attempts: number, + delayMs: number, + hasProviders = false, + ) => { for (let attempt = 0; attempt < attempts; attempt += 1) { - if (await adoptLocal()) return true; + if (await adoptLocal(hasProviders)) return true; if (attempt < attempts - 1) { await new Promise((resolve) => { window.setTimeout(resolve, delayMs); @@ -61,9 +74,9 @@ export default function AuthGuard({ children }: AuthGuardProps) { return false; }; - const holdForDesktop = async () => { + const holdForDesktop = async (hasProviders = false) => { while (!cancelled) { - if (await adoptLocal()) return; + if (await adoptLocal(hasProviders)) return; await new Promise((resolve) => { window.setTimeout(resolve, 400); }); @@ -75,10 +88,16 @@ export default function AuthGuard({ children }: AuthGuardProps) { const status = await authApi.getAuthStatus(); if (status.setup_required) { - if (await tryLocalSession(desktop ? 20 : 4, desktop ? 250 : 150)) + if ( + await tryLocalSession( + desktop ? 20 : 4, + desktop ? 250 : 150, + status.has_providers === true, + ) + ) return; if (desktop) { - await holdForDesktop(); + await holdForDesktop(status.has_providers === true); return; } clearAuthToken(); @@ -86,9 +105,25 @@ export default function AuthGuard({ children }: AuthGuardProps) { return; } + if ( + desktop && + desktopPostSessionPath(status.has_providers === true) === "/setup" + ) { + if (await tryLocalSession(20, 250, status.has_providers === true)) + return; + if (!cancelled) navigate("/setup", { replace: true }); + return; + } + const token = getAuthToken(); if (!token) { - if (await tryLocalSession(desktop ? 20 : 4, desktop ? 250 : 150)) + if ( + await tryLocalSession( + desktop ? 20 : 4, + desktop ? 250 : 150, + status.has_providers === true, + ) + ) return; if (desktop) { await holdForDesktop(); diff --git a/dashboard/src/locales/en.json b/dashboard/src/locales/en.json index 42b9699c..2cf602ca 100644 --- a/dashboard/src/locales/en.json +++ b/dashboard/src/locales/en.json @@ -4238,6 +4238,8 @@ }, "wizard": { "title": "Initial setup", + "desktopTitle": "Configure a model", + "checking": "Checking setup status…", "back": "Back", "sessionExpired": "Wizard session expired. Please verify the bootstrap password again.", "stepDatabase": "Choose database", @@ -4309,6 +4311,7 @@ "model": { "intro": "Configure one model provider so the assistant can reply. You can skip this step and configure it later.", "skip": "Skip", + "skipToWorkspace": "Skip and enter workspace", "presetTab": "Preset provider", "customTab": "Custom provider", "showMorePresets": "Show more providers ({{count}})", diff --git a/dashboard/src/locales/zh.json b/dashboard/src/locales/zh.json index 5543168e..6e255d06 100644 --- a/dashboard/src/locales/zh.json +++ b/dashboard/src/locales/zh.json @@ -4376,6 +4376,8 @@ }, "wizard": { "title": "初始化设置", + "desktopTitle": "配置模型", + "checking": "正在检查初始化状态…", "back": "上一步", "sessionExpired": "向导会话已过期,请重新验证启动密码。", "stepDatabase": "选择数据库", @@ -4448,6 +4450,7 @@ "model": { "intro": "配置一个模型提供商,助手就可以回应你了。你也可以跳过本步,稍后在「模型」页配置。", "skip": "跳过", + "skipToWorkspace": "跳过,进入工作台", "presetTab": "预置提供商", "customTab": "自定义提供商", "showMorePresets": "显示更多提供商({{count}} 个)", diff --git a/dashboard/src/pages/Login/index.test.tsx b/dashboard/src/pages/Login/index.test.tsx index c963eb88..84c3f07c 100644 --- a/dashboard/src/pages/Login/index.test.tsx +++ b/dashboard/src/pages/Login/index.test.tsx @@ -41,6 +41,7 @@ function renderLogin() { describe("LoginPage local session", () => { beforeEach(() => { localStorage.clear(); + sessionStorage.clear(); localSession.mockReset(); getAuthStatus.mockReset(); getOidcStatus.mockReset(); @@ -82,6 +83,37 @@ describe("LoginPage local session", () => { expect(screen.queryByText("usable app")).toBeNull(); }); + it("opens desktop first launch on model setup", async () => { + localSession.mockResolvedValue({ + access_token: "guest-token", + token_type: "Bearer", + expires_in: 3600, + user: { + id: 1, + username: "local", + role: "admin", + display_name: "FreeOS", + locale: "zh", + is_local: true, + }, + token: "guest-token", + }); + + render( + + + } /> + usable app} /> + model setup} /> + + , + ); + + expect(await screen.findByText("model setup")).toBeInTheDocument(); + expect(screen.queryByText("login form")).toBeNull(); + expect(getAuthToken()).toBe("guest-token"); + }); + it("does not render the login form inside the desktop shell", async () => { localSession.mockRejectedValue(new Error("interactive login required")); getAuthStatus.mockResolvedValue({ setup_required: false }); diff --git a/dashboard/src/pages/Login/index.tsx b/dashboard/src/pages/Login/index.tsx index 69e8ce00..90191b1f 100644 --- a/dashboard/src/pages/Login/index.tsx +++ b/dashboard/src/pages/Login/index.tsx @@ -13,6 +13,7 @@ import { import AuthForm, { type AuthFormMode } from "../../components/AuthForm"; import BrandMark from "../../components/BrandMark"; import { applyGuestLocale, applyUserLocale } from "../../utils/locale"; +import { desktopPostSessionPath } from "../../utils/desktopOnboarding"; import { isDesktopShell } from "../../utils/desktopShell"; export default function LoginPage() { @@ -37,7 +38,11 @@ export default function LoginPage() { const session = await authApi.localSession(); setAuthToken(session.access_token); await applyUserLocale(session.user.locale); - if (!cancelled) navigate("/chat", { replace: true }); + if (!cancelled) { + navigate(desktop ? desktopPostSessionPath() : "/chat", { + replace: true, + }); + } return; } catch { if (attempt < attempts - 1) { @@ -53,7 +58,9 @@ export default function LoginPage() { const session = await authApi.localSession(); setAuthToken(session.access_token); await applyUserLocale(session.user.locale); - if (!cancelled) navigate("/chat", { replace: true }); + if (!cancelled) { + navigate(desktopPostSessionPath(), { replace: true }); + } return; } catch { await new Promise((resolve) => { diff --git a/dashboard/src/pages/Setup/index.test.tsx b/dashboard/src/pages/Setup/index.test.tsx new file mode 100644 index 00000000..4cf90ba9 --- /dev/null +++ b/dashboard/src/pages/Setup/index.test.tsx @@ -0,0 +1,144 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; + +const getAuthStatus = vi.fn(); +const localSession = vi.fn(); +const isDesktopShell = vi.fn(); + +vi.mock("../../api/modules/auth", () => ({ + authApi: { + getAuthStatus: (...args: unknown[]) => getAuthStatus(...args), + localSession: (...args: unknown[]) => localSession(...args), + }, +})); + +vi.mock("../../utils/desktopShell", () => ({ + isDesktopShell: (...args: unknown[]) => isDesktopShell(...args), +})); + +vi.mock("../../utils/locale", () => ({ + storeUiLocale: vi.fn(), +})); + +vi.mock("../../i18n", () => ({ + ensureLocaleBundle: vi.fn(async () => undefined), +})); + +vi.mock("../../api/modules/preferences", () => ({ + preferencesApi: { setLocale: vi.fn(async () => undefined) }, +})); + +vi.mock("../../components/BrandMark", () => ({ + default: () =>
FreeOS
, +})); + +vi.mock("./steps/ModelStep", () => ({ + default: ({ + onSkip, + skipLabel, + hideBack, + }: { + onSkip: () => void; + skipLabel?: string; + hideBack?: boolean; + }) => ( +
+
model step
+ {hideBack ?
no back
:
has back
} + +
+ ), +})); + +vi.mock("./steps/PasswordStep", () => ({ + default: () =>
password step
, +})); + +vi.mock("./steps/DatabaseStep", () => ({ + default: () =>
database step
, +})); + +vi.mock("./steps/AdminStep", () => ({ + default: () =>
admin step
, +})); + +vi.mock("./steps/FinishStep", () => ({ + default: () =>
finish step
, +})); + +import SetupPage from "./index"; +import { isDesktopModelOnboardingDone } from "../../utils/desktopOnboarding"; + +function renderSetup() { + return render( + + + } /> + workspace} /> + login wall} /> + + , + ); +} + +describe("SetupPage desktop first-run", () => { + beforeEach(() => { + localStorage.clear(); + sessionStorage.clear(); + getAuthStatus.mockReset(); + localSession.mockReset(); + isDesktopShell.mockReset(); + }); + + it("shows only the model step and can skip into the workspace", async () => { + isDesktopShell.mockReturnValue(true); + getAuthStatus.mockResolvedValue({ + setup_required: true, + wizard_password_required: true, + desktop: true, + has_providers: false, + }); + localSession.mockResolvedValue({ + access_token: "guest-token", + user: { id: 1, username: "local", locale: "zh" }, + }); + + renderSetup(); + + expect(await screen.findByText("model step")).toBeInTheDocument(); + expect(screen.queryByText("password step")).toBeNull(); + expect(screen.queryByText("database step")).toBeNull(); + expect(screen.queryByText("admin step")).toBeNull(); + expect(screen.getByText("no back")).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "wizard.model.skipToWorkspace" }), + ).toBeInTheDocument(); + + const user = userEvent.setup(); + await user.click( + screen.getByRole("button", { name: "wizard.model.skipToWorkspace" }), + ); + expect(await screen.findByText("workspace")).toBeInTheDocument(); + expect(isDesktopModelOnboardingDone()).toBe(true); + }); + + it("keeps the server password wizard when not on desktop", async () => { + isDesktopShell.mockReturnValue(false); + getAuthStatus.mockResolvedValue({ + setup_required: true, + wizard_password_required: true, + desktop: false, + has_providers: false, + }); + + renderSetup(); + + expect(await screen.findByText("password step")).toBeInTheDocument(); + expect(screen.queryByText("model step")).toBeNull(); + await waitFor(() => expect(localSession).not.toHaveBeenCalled()); + }); +}); diff --git a/dashboard/src/pages/Setup/index.tsx b/dashboard/src/pages/Setup/index.tsx index 13747f09..bd28ee10 100644 --- a/dashboard/src/pages/Setup/index.tsx +++ b/dashboard/src/pages/Setup/index.tsx @@ -1,14 +1,21 @@ import { useCallback, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Segmented, Steps, Typography } from "antd"; +import { message } from "@/utils/antdMessage"; import { Lock, UserCog, Cpu, CheckCircle, Wand2, Database } from "lucide-react"; import { useTranslation } from "react-i18next"; import { ensureLocaleBundle } from "../../i18n"; import { storeUiLocale, type UiLocale } from "../../utils/locale"; import { authApi } from "../../api/modules/auth"; +import { setAuthToken } from "../../api/request"; import { preferencesApi } from "../../api/modules/preferences"; import BrandMark from "../../components/BrandMark"; +import { + desktopPostSessionPath, + markDesktopModelOnboardingDone, +} from "../../utils/desktopOnboarding"; +import { isDesktopShell } from "../../utils/desktopShell"; import DatabaseStep from "./steps/DatabaseStep"; import PasswordStep from "./steps/PasswordStep"; import AdminStep from "./steps/AdminStep"; @@ -32,6 +39,8 @@ export default function SetupPage() { const { t, i18n } = useTranslation(); const navigate = useNavigate(); const [checking, setChecking] = useState(true); + const [desktopFlow, setDesktopFlow] = useState(false); + const [saving, setSaving] = useState(false); const [passwordRequired, setPasswordRequired] = useState(true); const [current, setCurrentRaw] = useState(STEP_PASSWORD); const [adminCreds, setAdminCreds] = useState<{ @@ -62,12 +71,42 @@ export default function SetupPage() { return r.wizard_token; }, []); + const enterWorkspace = useCallback(() => { + markDesktopModelOnboardingDone(); + wizardSession.clearAll(); + navigate("/chat", { replace: true }); + }, [navigate]); + useEffect(() => { let cancelled = false; authApi .getAuthStatus() .then(async (status) => { if (cancelled) return; + const desktop = isDesktopShell() || status.desktop === true; + + if (desktop) { + if ( + desktopPostSessionPath(status.has_providers === true) === "/chat" + ) { + wizardSession.clearAll(); + navigate("/chat", { replace: true }); + return; + } + try { + const session = await authApi.localSession(); + setAuthToken(session.access_token); + wizardSession.saveSetupJwt(session.access_token); + } catch { + /* AuthGuard / login will keep retrying; stay on the model step */ + } + if (cancelled) return; + setDesktopFlow(true); + goToStep(STEP_MODEL); + setChecking(false); + return; + } + if (!status.setup_required) { wizardSession.clearAll(); navigate("/login", { replace: true }); @@ -161,7 +200,26 @@ export default function SetupPage() { goToStep(STEP_PASSWORD); }; - if (checking) { + const handleDesktopContinue = async (draft: ProviderDraft) => { + const token = + wizardSession.loadSetupJwt() || wizardSession.loadToken() || ""; + if (!token) { + message.error(t("wizard.sessionExpired")); + return; + } + setSaving(true); + try { + await wizardApi.finish({ provider_draft: draft }, token); + enterWorkspace(); + } catch (err) { + message.error( + err instanceof Error ? err.message : t("wizard.finish.providerFailed"), + ); + setSaving(false); + } + }; + + if (checking || saving) { return (
- Checking setup status… + + {saving ? t("wizard.finish.running") : t("wizard.checking")} +
); } @@ -229,7 +289,8 @@ export default function SetupPage() {
- {t("wizard.title")} + {" "} + {desktopFlow ? t("wizard.desktopTitle") : t("wizard.title")}
@@ -244,13 +305,15 @@ export default function SetupPage() { onChange={handleLanguageChange} /> - + {desktopFlow ? null : ( + + )}
- {current === STEP_PASSWORD && passwordRequired && ( - - )} - {current === STEP_DATABASE && ( - void handleDatabaseContinue()} - onBack={passwordRequired ? handleBackFromDatabase : undefined} - /> - )} - {current === STEP_ADMIN && ( - { - setAdminCreds(creds); - wizardSession.saveDraft({ adminUsername: creds.username }); - goToStep(STEP_MODEL); - }} - /> - )} - {current === STEP_MODEL && ( + {desktopFlow ? ( goToStep(STEP_ADMIN)} - onSkip={() => { - setProviderDraft(null); - wizardSession.saveDraft({}); - goToStep(STEP_FINISH); - }} + hideBack + skipLabel={t("wizard.model.skipToWorkspace")} + onBack={() => undefined} + onSkip={enterWorkspace} onContinue={(draft) => { - setProviderDraft(draft); - goToStep(STEP_FINISH); + void handleDesktopContinue(draft); }} /> - )} - {current === STEP_FINISH && adminCreds && ( - + ) : ( + <> + {current === STEP_PASSWORD && passwordRequired && ( + + )} + {current === STEP_DATABASE && ( + void handleDatabaseContinue()} + onBack={passwordRequired ? handleBackFromDatabase : undefined} + /> + )} + {current === STEP_ADMIN && ( + { + setAdminCreds(creds); + wizardSession.saveDraft({ adminUsername: creds.username }); + goToStep(STEP_MODEL); + }} + /> + )} + {current === STEP_MODEL && ( + goToStep(STEP_ADMIN)} + onSkip={() => { + setProviderDraft(null); + wizardSession.saveDraft({}); + goToStep(STEP_FINISH); + }} + onContinue={(draft) => { + setProviderDraft(draft); + goToStep(STEP_FINISH); + }} + /> + )} + {current === STEP_FINISH && adminCreds && ( + + )} + )}
diff --git a/dashboard/src/pages/Setup/steps/ModelStep.tsx b/dashboard/src/pages/Setup/steps/ModelStep.tsx index 0fab1698..2e52d29f 100644 --- a/dashboard/src/pages/Setup/steps/ModelStep.tsx +++ b/dashboard/src/pages/Setup/steps/ModelStep.tsx @@ -97,6 +97,8 @@ interface Props { onBack: () => void; onSkip: () => void; onContinue: (draft: ProviderDraft) => void; + hideBack?: boolean; + skipLabel?: string; } type SetupMode = "preset" | "custom"; @@ -146,7 +148,13 @@ const INPUT_TYPE_OPTIONS = [ { value: "audio", label: "inputTypeAudio" as const }, ]; -export default function ModelStep({ onBack, onSkip, onContinue }: Props) { +export default function ModelStep({ + onBack, + onSkip, + onContinue, + hideBack = false, + skipLabel, +}: Props) { const { t } = useTranslation(); const [presetForm] = Form.useForm(); const [customForm] = Form.useForm(); @@ -561,8 +569,8 @@ export default function ModelStep({ onBack, onSkip, onContinue }: Props) { const renderFooter = () => (
- - + {hideBack ? null : } +