From 54ddaa2cee00212b00fb3d972017a8d5b5e4d784 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 19 Apr 2026 16:11:02 -0400 Subject: [PATCH] feat(install): redesign web wizard and scope dev token to PAT installs Redesign the install wizard for clarity: - swap the sidebar layout for a centered column and a horizontal stepper - make completed/current stepper entries clickable links - reorder steps so Server URL precedes LLMs - use env-var placeholders (ANTHROPIC_API_KEY, etc.) with per-provider "Where do I get this?" disclosures - replace the readonly "Validated username" input with a success pill - drop the GitHub App name field (GitHub confirms the name anyway) - re-label the GitHub App option and split review rows by strategy - add a copy action to the Server URL on the review screen Scope the dev token to PAT installs: - only generate the dev token, write its files, and set FABRO_DEV_TOKEN inside the GithubInstallState::Token arm - mark dev_token optional on InstallFinishResponse in the OpenAPI spec - hide the Development token card on /install/finishing when absent - add app_install_finish_omits_dev_token_and_does_not_write_it test Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/fabro-web/app/install-app.tsx | 1174 ++++++---- apps/fabro-web/app/install-config.ts | 18 +- docs/api-reference/fabro-api.yaml | 5 +- lib/crates/fabro-server/src/install.rs | 57 +- .../fabro-server/tests/it/api/install.rs | 144 ++ lib/crates/fabro-spa/assets/assets/app.css | 2 +- .../fabro-spa/assets/assets/entry-70cq40fr.js | 1969 ----------------- .../fabro-spa/assets/assets/entry-rzgjexka.js | 1969 +++++++++++++++++ lib/crates/fabro-spa/assets/index.html | 2 +- .../src/models/install-finish-response.ts | 5 +- 10 files changed, 2947 insertions(+), 2398 deletions(-) delete mode 100644 lib/crates/fabro-spa/assets/assets/entry-70cq40fr.js create mode 100644 lib/crates/fabro-spa/assets/assets/entry-rzgjexka.js diff --git a/apps/fabro-web/app/install-app.tsx b/apps/fabro-web/app/install-app.tsx index 6c390ede2..1b625ceaa 100644 --- a/apps/fabro-web/app/install-app.tsx +++ b/apps/fabro-web/app/install-app.tsx @@ -1,6 +1,18 @@ import { startTransition, useEffect, useMemo, useState } from "react"; -import type { ReactNode } from "react"; +import type { FormEvent, ReactNode } from "react"; import { Link, Navigate, useLocation, useNavigate } from "react-router"; +import { + ArrowLeftIcon, + ArrowRightIcon, + ArrowTopRightOnSquareIcon, + CheckCircleIcon, + CheckIcon, + ChevronDownIcon, + ClipboardDocumentCheckIcon, + ClipboardIcon, + EyeIcon, + EyeSlashIcon, +} from "@heroicons/react/16/solid"; import { type InstallFinishResponse, @@ -18,7 +30,6 @@ import { testInstallGithubToken, testInstallLlm, } from "./install-api"; -import { AuthLayout } from "./components/auth-layout"; import { INSTALL_PROVIDERS } from "./install-config"; import { shouldRedirectAfterHealthPoll } from "./install-flow"; import { @@ -29,12 +40,14 @@ import { const INSTALL_STEPS = [ { id: "welcome", label: "Welcome", href: "/install/welcome" }, - { id: "llm", label: "LLM", href: "/install/llm" }, { id: "server", label: "Server", href: "/install/server" }, + { id: "llm", label: "LLMs", href: "/install/llm" }, { id: "github", label: "GitHub", href: "/install/github" }, { id: "review", label: "Review", href: "/install/review" }, ] as const; +const STEPPER_STEPS = INSTALL_STEPS.slice(1); + type StepId = (typeof INSTALL_STEPS)[number]["id"]; type FinishState = InstallFinishResponse | null; type GithubStrategy = "token" | "app"; @@ -54,12 +67,7 @@ type AppForm = { allowedUsername: string; }; -type ProviderSelection = Record< - string, - { - apiKey: string; - } ->; +type ProviderSelection = Record; export default function InstallApp() { const navigate = useNavigate(); @@ -246,9 +254,9 @@ export default function InstallApp() { if (sessionState.status === "loading") { return ( - + Reading the current install state from the server. - + ); } @@ -278,11 +286,11 @@ export default function InstallApp() { ) : location.pathname === "/install/llm" ? ( { const providers = INSTALL_PROVIDERS.map(({ id }) => { const current = llmSelection[id] ?? { apiKey: "" }; @@ -306,7 +314,7 @@ export default function InstallApp() { await putInstallLlm(installToken, providers); const nextSession = await getInstallSession(installToken); setSessionState({ status: "ready", data: nextSession }); - navigate("/install/server"); + navigate("/install/github"); } catch (error) { setSaveError( error instanceof Error ? error.message : "Failed to save LLM settings.", @@ -320,11 +328,11 @@ export default function InstallApp() { ) : location.pathname === "/install/server" ? ( { if (!canonicalUrl.trim()) { setSaveError("Enter the canonical server URL before continuing."); @@ -336,7 +344,7 @@ export default function InstallApp() { await putInstallServer(installToken, canonicalUrl.trim()); const nextSession = await getInstallSession(installToken); setSessionState({ status: "ready", data: nextSession }); - navigate("/install/github"); + navigate("/install/llm"); } catch (error) { setSaveError( error instanceof Error ? error.message : "Failed to save server settings.", @@ -348,13 +356,17 @@ export default function InstallApp() { > setCanonicalUrl(event.target.value)} className={INPUT_CLASS} placeholder="https://fabro.example.com" + autoComplete="url" + spellCheck={false} /> @@ -362,12 +374,12 @@ export default function InstallApp() { ) : location.pathname === "/install/github" ? ( { setSubmitting(true); setSaveError(null); @@ -421,29 +433,43 @@ export default function InstallApp() { > {githubStrategy === "token" ? ( - <> - -