diff --git a/CHANGELOG.md b/CHANGELOG.md index b78c005..f1b71b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to Wafersmith are recorded here. The format follows ### Added +- **Release rehearsal** ([docs/release-rehearsal.md](docs/release-rehearsal.md)): the counter, UART + receiver and FIFO starters taken to a clean layout in the browser, timed, with no code typed and + no AI: 1 to 4 minutes each, every case tried and every planted mistake caught. - **Find examples for everything**: Health searches for examples for every untried case and every planted mistake that slipped through at once. Each example found is checked against the targets still open, so one example covers several (on the up/down counter with one example, 2 examples @@ -149,6 +152,11 @@ All notable changes to Wafersmith are recorded here. The format follows ### Fixed +- The sign-in no longer runs out after 30 minutes: the web app renews its access token with the + refresh token before it expires, without leaving the page. Before, the first request after + expiry reloaded the page through the sign-in server and lost what was on screen. +- Choosing a starter suggests its title as the project name; clicking into the name now selects + it, so typing replaces it instead of adding to it. - The Blocks step no longer throws away an unsaved chip when the project changes underneath it (a commit by someone else, or adding a block). - The spec check refuses an example that runs longer than the simulator's 50 µs limit (it could diff --git a/README.md b/README.md index 7d1cfe0..80a88dd 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Then open http://localhost:5173. | [Pilot deployment](deploy/pilot/README.md) | One-host HTTPS deployment, backups and restore | | [Security review](docs/security-review.md) | What was reviewed, fixed and accepted before the pilot | | [Phase 0 gate](docs/phase0-gate.md) | The first end-to-end milestone | +| [Release rehearsal](docs/release-rehearsal.md) | Three starters to a clean layout, timed, before 0.1.0 | Contributions follow [CONTRIBUTING.md](CONTRIBUTING.md); report vulnerabilities as described in [SECURITY.md](SECURITY.md). diff --git a/apps/web/src/auth.ts b/apps/web/src/auth.ts index b3cc182..1c09083 100644 --- a/apps/web/src/auth.ts +++ b/apps/web/src/auth.ts @@ -14,10 +14,28 @@ export const userManager = new UserManager({ userStore: new WebStorageStateStore({ store: window.sessionStorage }), }); +/** Renew this long before the access token expires, so a request doesn't go out with one about to. + * Rauthy accepts a refresh token only from 60 s before the access token expires. */ +const RENEW_BEFORE_S = 45; +let renewing: Promise | null = null; + +/** The signed-in user with a usable access token, or null. A token that has expired or is about to + * is renewed with the refresh token, without leaving the page (a sign-in redirect would lose + * unsaved work); only when that fails does the caller have to sign in again. */ export async function currentUser(): Promise { const user = await userManager.getUser(); - if (!user || user.expired) return null; - return user; + if (!user) return null; + if ((user.expires_in ?? 0) > RENEW_BEFORE_S) return user; + if (!user.refresh_token) return user.expired ? null : user; + renewing ??= userManager + .signinSilent() + .catch(() => null) + .finally(() => { + renewing = null; + }); + const renewed = await renewing; + if (renewed && !renewed.expired) return renewed; + return user.expired ? null : user; } export function login(returnTo: string = window.location.pathname) { diff --git a/apps/web/src/pages/Projects.tsx b/apps/web/src/pages/Projects.tsx index 01525bd..99c450e 100644 --- a/apps/web/src/pages/Projects.tsx +++ b/apps/web/src/pages/Projects.tsx @@ -49,6 +49,9 @@ export function ProjectsPage() { placeholder="Project name, e.g. UART transmitter" value={name} onChange={(e) => setName(e.target.value)} + // A name suggested from a starter is selected on click, so typing replaces it. + onFocus={(e) => starter && name === starter.title && e.target.select()} + onMouseUp={(e) => starter && name === starter.title && e.preventDefault()} autoFocus />