feat(FN-2407): add blocking auth token recovery flow

- Emit an auth-token recovery event when dashboard daemon calls fail with bearer-token 401 responses
- Render a non-dismissable recovery dialog with set-token and clear-token reload actions
- Keep the recovery overlay topmost while aligning modal markup with shared modal conventions
- Add focused regression coverage for dialog behavior and include a patch changeset for @runfusion/fusion
This commit is contained in:
Fusion
2026-04-25 01:22:01 -07:00
committed by gsxdsm
parent 39ec162c13
commit dbc9446a32
5 changed files with 21 additions and 11 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Add a blocking dashboard token-recovery dialog that appears only for daemon bearer-token 401 responses, with set-token or clear-token recovery actions that reload the app.

View File

@@ -2,13 +2,8 @@
z-index: 210;
}
.auth-token-recovery-modal {
width: min(560px, calc(100vw - (var(--space-xl) * 2)));
}
.auth-token-recovery-header h2 {
.auth-token-recovery-header h3 {
margin: 0;
font-size: 1.1rem;
}
.auth-token-recovery-content {

View File

@@ -44,14 +44,14 @@ export function AuthTokenRecoveryDialog({ open }: AuthTokenRecoveryDialogProps)
}}
>
<div
className="modal auth-token-recovery-modal"
className="modal modal-md"
role="dialog"
aria-modal="true"
aria-labelledby="auth-token-recovery-title"
aria-describedby="auth-token-recovery-description"
>
<div className="modal-header auth-token-recovery-header">
<h2 id="auth-token-recovery-title">Authentication token required</h2>
<h3 id="auth-token-recovery-title">Authentication token required</h3>
</div>
<div className="auth-token-recovery-content">

View File

@@ -29,9 +29,19 @@ describe("AuthTokenRecoveryDialog", () => {
it("renders a blocking dialog with disabled set button until token is entered", () => {
render(<AuthTokenRecoveryDialog open={true} />);
expect(screen.getByRole("dialog", { name: "Authentication token required" })).toBeInTheDocument();
const dialog = screen.getByRole("dialog", { name: "Authentication token required" });
expect(dialog).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /close/i })).toBeNull();
const overlay = dialog.closest(".auth-token-recovery-overlay");
expect(overlay).toBeTruthy();
if (!overlay) {
throw new Error("Expected auth token recovery overlay");
}
expect(dialog.className).toContain("modal-md");
const setTokenButton = screen.getByRole("button", { name: "Set token and reload" });
expect(setTokenButton).toBeDisabled();

View File

@@ -2361,7 +2361,7 @@ describe("ModelOnboardingModal", () => {
expect(mockFetchGlobalSettings).toHaveBeenCalled();
// The model dropdown should be pre-populated with the saved default
const dropdown = screen.getByTestId("mock-model-dropdown") as HTMLSelectElement;
const dropdown = await screen.findByTestId("mock-model-dropdown") as HTMLSelectElement;
expect(dropdown.value).toBe("anthropic/claude-sonnet-4-5");
});
@@ -2407,7 +2407,7 @@ describe("ModelOnboardingModal", () => {
});
// The model dropdown should be pre-populated with the saved default
const dropdown = screen.getByTestId("mock-model-dropdown") as HTMLSelectElement;
const dropdown = await screen.findByTestId("mock-model-dropdown") as HTMLSelectElement;
expect(dropdown.value).toBe("openai/gpt-4o");
});
});