diff --git a/webui/src/App.tsx b/webui/src/App.tsx
index ee035c2f..1eb19792 100644
--- a/webui/src/App.tsx
+++ b/webui/src/App.tsx
@@ -411,7 +411,7 @@ export default function App() {
if (cancelled) return;
const msg = (e as Error).message;
if (msg.includes("HTTP 401") || msg.includes("HTTP 403")) {
- setState({ status: "auth", failed: true });
+ setState({ status: "auth", failed: !!secret });
} else {
setState({ status: "error", message: msg });
}
@@ -433,7 +433,7 @@ export default function App() {
} catch (e) {
const msg = (e as Error).message;
if (msg.includes("HTTP 401") || msg.includes("HTTP 403")) {
- setState({ status: "auth", failed: true });
+ setState({ status: "auth", failed: !!bootstrapSecretRef.current });
}
}
}, tokenRefreshDelayMs(state.tokenExpiresAt));
diff --git a/webui/src/tests/app-layout.test.tsx b/webui/src/tests/app-layout.test.tsx
index 687660ed..0a3fa8ba 100644
--- a/webui/src/tests/app-layout.test.tsx
+++ b/webui/src/tests/app-layout.test.tsx
@@ -256,6 +256,34 @@ describe("App layout", () => {
vi.useRealTimers();
});
+ it("shows the auth form without an invalid-password error on first load", async () => {
+ vi.mocked(fetchBootstrap).mockRejectedValueOnce(
+ new Error("bootstrap failed: HTTP 401"),
+ );
+
+ render();
+
+ expect(await screen.findByText("Authentication required")).toBeInTheDocument();
+ expect(screen.queryByText("Invalid password. Try again.")).not.toBeInTheDocument();
+ expect(connectSpy).not.toHaveBeenCalled();
+ });
+
+ it("shows an invalid-password error after a submitted password is rejected", async () => {
+ vi.mocked(fetchBootstrap).mockRejectedValue(
+ new Error("bootstrap failed: HTTP 401"),
+ );
+
+ render();
+
+ const password = await screen.findByPlaceholderText("Password");
+ fireEvent.change(password, { target: { value: "wrong-password" } });
+ fireEvent.click(screen.getByRole("button", { name: "Connect" }));
+
+ expect(await screen.findByText("Invalid password. Try again.")).toBeInTheDocument();
+ expect(fetchBootstrap).toHaveBeenLastCalledWith("", "wrong-password");
+ expect(connectSpy).not.toHaveBeenCalled();
+ });
+
it("keeps sidebar layout out of the main thread width contract", async () => {
const { container } = render();