From 576c8fe8c6fbc7c635ed72b2f9198621819dcfba Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Thu, 24 Sep 2026 13:52:22 +0800 Subject: [PATCH] Simplify administrator setup and make introduction fullscreen --- CONTRIBUTING.md | 16 ++++--- README.md | 5 +-- apps/web/e2e/first-run.spec.ts | 41 +++++++++++++---- apps/web/src/App.tsx | 17 ++++--- .../src/features/first-run/ConsoleAccess.tsx | 18 +------- .../src/features/first-run/FirstRunHome.css | 35 ++++++++------- .../src/features/first-run/FirstRunHome.tsx | 15 ++++--- .../src/features/first-run/console-access.css | 6 +-- apps/web/src/lib/console-auth-strings.ts | 5 +-- deploy/install/configuration.py | 2 - deploy/install/install.py | 10 ++--- deploy/install/test_install.py | 22 +++------ docs/getting-started/install.md | 15 +++---- services/core-console/account_store.go | 2 +- services/core-console/account_store_test.go | 14 +++--- services/core-console/auth.go | 24 ++++------ services/core-console/auth_test.go | 45 +++++++++---------- services/core-console/config.go | 8 +--- 18 files changed, 136 insertions(+), 164 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ca10ec02..7adab337e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1750,10 +1750,10 @@ unchanged to Core, without borrowing the console's caller or administrator key. The same origin, path, method and transport restrictions still apply. Account mode is explicit (`CORE_CONSOLE_AUTH_MODE=account`) and requires a private -setup-key file and a private writable state directory. Only the installation's -one-time setup credential can claim the administrator account. The atomic durable -account record stores a password hash; corruption or a missing required credential -must never reopen registration. Account creation is race-safe. Cookie sessions are +writable state directory. The first visitor registers the sole administrator with +a username and password; no initialization key is required. The atomic durable +account record stores a password hash; corruption or missing registered account +state must never reopen registration. Account creation is race-safe. Cookie sessions are bounded, HttpOnly, SameSite Strict and Secure for HTTPS origins; a restart requires sign-in again, without deleting the account. Unauthenticated access is limited to the static login UI, finite console authentication routes and the existing @@ -1786,10 +1786,12 @@ access step. Web-only consoles with `api_keys: false` instead explain how to use an existing Core key and allow the introduction to continue without key-management requests. A failed or malformed capability read must not imply either capability. -First-run Home is a skippable/replayable console introduction after account setup. -It does not change public Core resource semantics or block ordinary administration. +First-run Home is a standalone full-screen, skippable/replayable tutorial after +account setup, outside the console shell. Setup and the introduction have no +sidebar. Respect reduced-motion preferences throughout. The introduction does not +change public Core resource semantics or block ordinary administration. Keep new onboarding state and components outside the oversized `App.tsx`. Persist -only non-secret presentation progress; password, setup key and model provider key +only non-secret presentation progress; password and model provider key must not enter browser storage or generated code samples. Creating a saved Agent is an explicit write through the existing API. Reconcile uncertain results before another write, and associate external examples with their exact metadata marker, diff --git a/README.md b/README.md index 4763e5ce5..050bea46a 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,8 @@ existing Sessions retain their node across disconnects and resume. This starts Core, Web and PostgreSQL with zero execution nodes. Public release bundles are not published yet; see the [installation guide](docs/getting-started/install.md) for building a bundle and the host/network prerequisites. -2. **Sign in to Web.** Open the console address printed by the installer. Use - the one-time key in `~/.parsar/core/config/console.setup.key` to register your - administrator account, then keep your chosen username and password safe. +2. **Sign in to Web.** Open the console address printed by the installer and + register your administrator account with a username and password. Keep them safe. Existing installations retain their `admin` / `console.password` login. The console connects to Core automatically. During first-run, create and save an Agent API key for requests from your own machine or application. diff --git a/apps/web/e2e/first-run.spec.ts b/apps/web/e2e/first-run.spec.ts index 614bcb2a4..ec78fdc80 100644 --- a/apps/web/e2e/first-run.spec.ts +++ b/apps/web/e2e/first-run.spec.ts @@ -45,10 +45,14 @@ test.beforeEach(async ({ page, request }) => { test("validates administrator setup before sending credentials and keeps only nonsecret progress", async ({ page }) => { const writes = await mockAccount(page, { mode: "setup" }); + await page.setViewportSize({ width: 1440, height: 1000 }); await page.context().grantPermissions(["clipboard-read", "clipboard-write"]); await page.goto("/"); await expect(page.getByRole("heading", { name: "Create your administrator account" })).toBeVisible(); - await page.getByLabel(/^Setup key/).fill("fixture-setup-secret"); + await expect(page.locator(".app-sidebar")).toHaveCount(0); + await expect(page.getByLabel(/^Setup key/)).toHaveCount(0); + await expect(page.getByText("PARSAR / CORE", { exact: true })).toHaveCount(0); + await expect(page.getByText("01 — You manage this cloud.", { exact: true })).toHaveCount(0); await fillAccount(page); await page.getByLabel("Confirm password", { exact: true }).fill("different-password"); await page.getByRole("button", { name: "Create administrator account", exact: true }).click(); @@ -63,16 +67,19 @@ test("validates administrator setup before sending credentials and keeps only no await page.getByLabel("Confirm password", { exact: true }).fill(password); await page.getByRole("button", { name: "Create administrator account", exact: true }).click(); await expect(page.getByRole("heading", { name: "Keep your sign-in details." })).toBeVisible(); - expect(writes).toEqual([{ action: "setup", body: { username, password, setup_key: "fixture-setup-secret" } }]); + await expect(page.locator(".app-sidebar")).toHaveCount(0); + expect(writes).toEqual([{ action: "setup", body: { username, password } }]); + await expect(page.locator(".app-shell")).toHaveCount(0); + await expect(page.locator(".first-run-home")).toHaveCSS("width", "1440px"); + await expect(page.locator(".first-run-home")).toHaveCSS("height", "1000px"); + await expect(page.getByRole("button", { name: "Skip introduction", exact: true })).toBeVisible(); await page.getByRole("button", { name: "Copy sign-in details", exact: true }).click(); const copied = await page.evaluate(() => navigator.clipboard.readText()); expect(copied).toContain(new URL(page.url()).origin); expect(copied).toContain(username); expect(copied).not.toContain(password); - expect(copied).not.toContain("fixture-setup-secret"); const storage = await page.evaluate(() => JSON.stringify({ ...localStorage, ...sessionStorage })); expect(storage).not.toContain(password); - expect(storage).not.toContain("fixture-setup-secret"); }); test("remembers the introduction step and dismissal across reloads and sign-in, and allows replay", async ({ page }) => { @@ -86,6 +93,7 @@ test("remembers the introduction step and dismissal across reloads and sign-in, await expect(page.getByRole("heading", { name: "Make your first API request." })).toBeVisible(); await page.getByRole("button", { name: "Skip introduction", exact: true }).first().click(); await expect(page.locator(".first-run-home")).toHaveCount(0); + await expect(page.locator(".app-sidebar")).toBeVisible(); await page.reload(); await expect(page.getByRole("button", { name: "Sign out", exact: true })).toBeVisible(); await expect(page.locator(".first-run-home")).toHaveCount(0); @@ -101,6 +109,25 @@ test("remembers the introduction step and dismissal across reloads and sign-in, expect(writes.map(({ action }) => action)).toEqual(["logout", "login"]); }); +test("keeps tutorial preferences inside the viewport and restores console navigation when skipped", async ({ page }) => { + await mockAccount(page, { mode: "authenticated", username }); + await page.goto("/"); + await page.locator(".first-run-toolbar .appearance-menu-trigger").click(); + const menu = page.getByRole("menu"); + const bounds = await menu.boundingBox(); + expect(bounds).not.toBeNull(); + expect(bounds!.y).toBeGreaterThanOrEqual(0); + expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(page.viewportSize()!.height); + await page.getByRole("menuitemradio", { name: "Dark theme", exact: true }).click(); + await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); + await page.locator(".first-run-toolbar .appearance-menu-trigger").click(); + await page.getByRole("menuitemradio", { name: "简体中文", exact: true }).click(); + await expect(page.getByRole("heading", { name: "保存你的登录信息。" })).toBeVisible(); + await page.getByRole("button", { name: "跳过导览", exact: true }).click(); + await expect(page.locator(".app-sidebar")).toBeVisible(); + await expect(page.locator(".first-run-home")).toHaveCount(0); +}); + test("keeps private Core requests unmounted on an authentication network failure", async ({ page, request }) => { await page.route("**/console/auth", (route) => route.abort("failed")); await page.goto("/"); @@ -124,7 +151,6 @@ test("does not replay an uncertain registration and reconciles the account befor return route.abort("failed"); }); await page.goto("/"); - await page.getByLabel(/^Setup key/).fill("fixture-setup-secret"); await fillAccount(page); await page.getByLabel("Confirm password", { exact: true }).fill(password); await page.getByRole("button", { name: "Create administrator account", exact: true }).click(); @@ -141,15 +167,14 @@ test("supports Chinese setup and reduced-motion introduction", async ({ page }) await page.emulateMedia({ reducedMotion: "reduce" }); await page.setViewportSize({ width: 1080, height: 900 }); await page.goto("/"); - await page.getByRole("combobox", { name: "Console language" }).selectOption("zh"); + await page.getByRole("combobox", { name: "Console language" }).selectOption("zh-CN"); await expect(page.getByRole("heading", { name: "创建管理员账户" })).toBeVisible(); - await page.getByLabel(/^初始化密钥/).fill("fixture-setup-secret"); await page.getByLabel(/^管理员用户名/).fill(username); await page.getByLabel(/^密码/).fill(password); await page.getByLabel("确认密码", { exact: true }).fill(password); await page.getByRole("button", { name: "创建管理员账户", exact: true }).click(); await expect(page.getByRole("heading", { name: "保存你的登录信息。" })).toBeVisible(); - await expect(page.locator(".first-run-home")).toHaveAttribute("lang", "zh"); + await expect(page.locator(".first-run-home")).toHaveAttribute("lang", "zh-CN"); const layout = await page.locator(".first-run-stage-content").evaluate((element) => ({ animation: getComputedStyle(element).animationName, width: document.documentElement.scrollWidth, diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 786328001..7765467be 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -2281,7 +2281,13 @@ export function App() { }, []); return ( -
+ {showIntroduction ? { void refreshAgents(); }} + onOpenAgent={(id) => { setIntroductionAgentId(id); void refreshAgents(); setView("agents"); }} + /> :
{t("skipToContent")}