fix(setup): resolve setup state before first paint (VAPP-85) #93

Merged
longjacksonle merged 1 commit from fix/VAPP-85-setup-page-flash into main 2026-07-17 04:08:25 +02:00

Fixes the flash when creating the first admin user. Originally filed as VA-140 against vervain-agent, which has no password-creation code at all; the screen is this repo's pages/setup.rs.

The bug

get_setup_state decides which of three shells a visitor belongs in (onboarding, sign-in, or the app), so it has to be answered before anything paints. It was fetched with use_resource, which is client-only: SSR emits the not-yet-resolved arm, the browser hydrates that placeholder, and real content swaps in a round-trip later. Onboarding took that hit twice, from two separate fetches of the same endpoint:

  • Layout painted the whole shell (sidebar, topbar, statusbar, dashboard) and only then bounced to /setup from a use_effect, so the visitor watched a full app they have no account for get yanked away.
  • /setup then rendered its own "Checking setup…" card before swapping in the password form. That card is centered and auto-height, so the swap moved and resized it.

Every other page already uses use_server_future(...)?; the setup path was the one that did not. It is the same failure mode as VAPP-37 (paint-then-correct), one level up.

The fix

A SetupGate above the router does the fetch once with use_server_future, which SSR resolves and serializes into the hydrated page, and publishes it as a SetupResource context. Layout and the setup page read that resolved value instead of each fetching, so both render final content on the first paint. Layout also declines to paint at all for a visitor it is about to redirect, and its session check moves to use_server_future for the same reason (has_session reads the request cookie via FullstackContext, which SSR populates).

One wrinkle worth flagging for review: the gate is fetched per page load and does not remount on navigation, so registration would leave it reporting a stale NeedsFirstUser and the shell would bounce the brand-new admin right back to /setup. FirstUserForm refreshes it before navigating, and holds submitting through the redirect so the button stays at "Creating…".

Verification

Driven against the real server-rendered HTML with a stub /api/setup, on both branches, so the before-state is reproduced rather than assumed.

Request Before (main) After
GET /setup, onboarding pending "Checking setup…", 0 password inputs the form, 2 password inputs, no placeholder
GET /, onboarding pending full shell server-rendered (sidebar, topbar, statusbar) no shell painted
/api/setup fetches per load 2 (both client-side, after paint) 1 (server-side, before paint)

Regression checks with setup Complete: a request with a session cookie still gets the shell, one without still gets none, and /setup still renders the "Setup finished" arm.

setup_gate_guard.rs fails the build if either consumer returns to use_resource or the router leaves the gate, the same way layout_guard pins VAPP-37, since the regression compiles and still shows the right thing eventually.

fmt, wasm and server checks, and 90 unit tests pass. The six clippy::unnecessary_sort_by errors in server_fns/mod.rs are pre-existing: they reproduce on a clean main and come from my local toolchain being newer than CI's.

Not covered

I have not watched this in a browser. The evidence above is the served HTML, which is where the flash originates, so a load-time flash is addressed. If "flickering" turns out to mean a repeating flicker or one while typing, that is a different bug and this does not fix it.

Fixes the flash when creating the first admin user. Originally filed as VA-140 against `vervain-agent`, which has no password-creation code at all; the screen is this repo's `pages/setup.rs`. ## The bug `get_setup_state` decides which of three shells a visitor belongs in (onboarding, sign-in, or the app), so it has to be answered before anything paints. It was fetched with `use_resource`, which is client-only: SSR emits the not-yet-resolved arm, the browser hydrates that placeholder, and real content swaps in a round-trip later. Onboarding took that hit twice, from two separate fetches of the same endpoint: - `Layout` painted the whole shell (sidebar, topbar, statusbar, dashboard) and only then bounced to `/setup` from a `use_effect`, so the visitor watched a full app they have no account for get yanked away. - `/setup` then rendered its own "Checking setup…" card before swapping in the password form. That card is centered and auto-height, so the swap moved and resized it. Every other page already uses `use_server_future(...)?`; the setup path was the one that did not. It is the same failure mode as VAPP-37 (paint-then-correct), one level up. ## The fix A `SetupGate` above the router does the fetch once with `use_server_future`, which SSR resolves and serializes into the hydrated page, and publishes it as a `SetupResource` context. `Layout` and the setup page read that resolved value instead of each fetching, so both render final content on the first paint. `Layout` also declines to paint at all for a visitor it is about to redirect, and its session check moves to `use_server_future` for the same reason (`has_session` reads the request cookie via `FullstackContext`, which SSR populates). One wrinkle worth flagging for review: the gate is fetched per page load and does not remount on navigation, so registration would leave it reporting a stale `NeedsFirstUser` and the shell would bounce the brand-new admin right back to `/setup`. `FirstUserForm` refreshes it before navigating, and holds `submitting` through the redirect so the button stays at "Creating…". ## Verification Driven against the real server-rendered HTML with a stub `/api/setup`, on both branches, so the before-state is reproduced rather than assumed. | Request | Before (`main`) | After | | --- | --- | --- | | `GET /setup`, onboarding pending | "Checking setup…", **0** password inputs | the form, **2** password inputs, no placeholder | | `GET /`, onboarding pending | full shell server-rendered (sidebar, topbar, statusbar) | no shell painted | | `/api/setup` fetches per load | 2 (both client-side, after paint) | 1 (server-side, before paint) | Regression checks with setup `Complete`: a request with a session cookie still gets the shell, one without still gets none, and `/setup` still renders the "Setup finished" arm. `setup_gate_guard.rs` fails the build if either consumer returns to `use_resource` or the router leaves the gate, the same way `layout_guard` pins VAPP-37, since the regression compiles and still shows the right thing eventually. fmt, wasm and server checks, and 90 unit tests pass. The six `clippy::unnecessary_sort_by` errors in `server_fns/mod.rs` are pre-existing: they reproduce on a clean `main` and come from my local toolchain being newer than CI's. ## Not covered I have not watched this in a browser. The evidence above is the served HTML, which is where the flash originates, so a load-time flash is addressed. If "flickering" turns out to mean a *repeating* flicker or one *while typing*, that is a different bug and this does not fix it.
fix(setup): resolve setup state before first paint (VAPP-85)
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 2m33s
Create release / Create release from merged PR (pull_request) Has been skipped
172ab7e2db
Creating the first admin flashed on the way in. `get_setup_state` decides which of three shells a visitor belongs in (onboarding, sign-in, or the app), but it was fetched with `use_resource`, which is client-only: SSR emits the not-yet-resolved arm, the browser hydrates that placeholder, and the real content swaps in a round-trip later. Onboarding took that hit twice over, from two separate fetches of the same endpoint. `Layout` painted the entire shell (sidebar, topbar, statusbar, dashboard) and only then bounced to `/setup` from a `use_effect`, so the visitor watched a full app they have no account for get yanked away; `/setup` then rendered its own "Checking setup…" card before swapping in the password form, and because that card is centered and auto-height, the swap moved and resized it.

Move the fetch to a `SetupGate` above the router (`setup_gate.rs`): one `use_server_future`, which resolves during SSR and serializes the answer into the hydrated page, published as a `SetupResource` context. `Layout` and the setup page now read that resolved value instead of each fetching, so both render their final content on the first paint. `Layout` additionally declines to paint at all for a visitor it is about to redirect, and its session check moves to `use_server_future` for the same reason (`has_session` reads the request cookie through `FullstackContext`, which SSR populates, so a signed-in visitor is known before paint rather than after). Same fix as VAPP-37, one level up: resolve before painting instead of paint-then-correct.

The gate is fetched once per page load and does not remount on navigation, so registration would leave it reporting a stale `NeedsFirstUser` and the shell would bounce the brand-new admin straight back to `/setup`. `FirstUserForm` refreshes it before navigating, and holds `submitting` through the redirect so the button stays at "Creating…" instead of flicking back to "Create user".

Verified against the real server-rendered HTML with a stub `/api/setup`, before and after. Before: `GET /setup` shipped "Checking setup…" with zero password inputs, and `GET /` server-rendered the full shell (sidebar, topbar, statusbar) despite onboarding being pending. After: `GET /setup` ships the form itself (both password inputs, no placeholder), `GET /` paints no shell, and each is one `/api/setup` fetch. Regression checks with setup `Complete`: a request carrying a session cookie still gets the shell, one without it still gets none, and `/setup` still renders the "Setup finished" arm. `setup_gate_guard.rs` fails the build if either consumer goes back to `use_resource` or the router leaves the gate, matching how `layout_guard` pins VAPP-37. fmt, wasm and server checks, and 90 unit tests pass; the six `clippy::unnecessary_sort_by` errors in `server_fns/mod.rs` predate this branch and reproduce on a clean `main` with the newer local toolchain.

#VAPP-85

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KYXyUTntEcVJaTwn5pody
Signed-off-by: longjacksonle <longjacksonle@gmail.com>
longjacksonle deleted branch fix/VAPP-85-setup-page-flash 2026-07-17 04:08:25 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-apps!93
No description provided.