feat(setup): install-wizard UI (epic 6c)

- Multi-step first-run wizard (intro → admin → Google → SMTP → finish), shown by App while the
  instance reports configured=false; the one-time token comes from the setup URL (?token=) and is
  sent as X-Setup-Token on every step. Google/SMTP steps appear only when secrets can be stored.
- App holds the 'me' query until setup status is known (so it doesn't 503 against the setup lock);
  a missing/invalid token shows a 'use the link from the logs' screen. EN/HU/DE.
This commit is contained in:
npeter83 2026-06-21 01:11:16 +02:00
parent 2689173208
commit 6bbadbf32f
6 changed files with 461 additions and 0 deletions

View file

@ -22,6 +22,7 @@ import { hintsEnabled, setHintsEnabled } from "./lib/hints";
import { useConfirm } from "./components/ConfirmProvider";
import type { PrefsController } from "./components/SettingsPanel";
import Welcome from "./components/Welcome";
import SetupWizard from "./components/SetupWizard";
import Header from "./components/Header";
import NavSidebar from "./components/NavSidebar";
import Sidebar from "./components/Sidebar";
@ -265,6 +266,15 @@ export default function App() {
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const queryClient = useQueryClient();
// First-run install gate: a fresh instance reports configured=false and runs in setup mode.
// We check this before anything else and render the wizard; `me` is held back until we know the
// instance is set up (otherwise it would 503 against the setup-mode lock).
const setupQuery = useQuery({
queryKey: ["setup-status"],
queryFn: api.setupStatus,
staleTime: Infinity,
});
const needsSetup = setupQuery.data?.configured === false;
// Poll the session so an account suspended/deleted by an admin is noticed even with no
// interaction (option 1): the refetch 401s → the 401 handler below drops to the login page.
// Only poll while signed in (data present) — polling on the public login page is pointless and
@ -272,6 +282,7 @@ export default function App() {
const meQuery = useQuery({
queryKey: ["me"],
queryFn: api.me,
enabled: setupQuery.isSuccess && !needsSetup,
refetchInterval: (query) => (query.state.data ? 60_000 : false),
});
@ -445,6 +456,13 @@ export default function App() {
// (we can't show our in-app confirm there), and unsaved prefs are local-only — they
// revert to the saved server baseline on the next load — so there's nothing to lose.
// First-run: a fresh instance is in setup mode (all other routes are locked) → show the wizard.
if (setupQuery.isLoading)
return (
<div className="min-h-screen grid place-items-center text-muted">{t("common.loading")}</div>
);
if (needsSetup) return <SetupWizard />;
if (meQuery.isLoading)
return (
<div className="min-h-screen grid place-items-center text-muted">{t("common.loading")}</div>