TehRiehlDeal 92d7dfdef3 fix(frontend): repair session teardown and effect lifecycles
Closes WP-10 of the audit: M10-M16, M18, M19, M20 and the client half of
H11. Steps 5 and 7 of the package were already done in WP-1.

An expired token left the app half signed in: the interceptor only wrote to
the console, so the nav still showed the user's name while every request
failed. A 401 now clears the token and navigates to sign-in - a full
navigation rather than a router push, so in-memory state and open sockets go
with the page, and the API client does not have to reach into the stores and
join an import cycle with them. A 401 from /auth/login is excluded: there,
it is an answer.

Signing out disconnected the notifications socket and nothing else. Chat,
versus and game night stayed open holding a token that had just been removed,
and the friends and game stores kept their contents, so signing in as someone
else on the same tab showed the previous person's friend list. Teardown now
lives in one place, because the failure mode was precisely that adding a
socket did not mean remembering to close it.

All four connect() guards tested `.connected` rather than existence, so a
socket that was mid-reconnect was replaced without being closed and the
orphan went on handling every event a second time. disconnect() likewise
cleared only the socket, leaving matchId, result and score behind - the next
match opened showing the previous match's completion modal.

Effects depended on the whole `user` object, which loadUser() rebuilds on
every call, so they re-ran when nothing had changed. In AsyncVersusGame that
effect's cleanup calls resetGame(), so reloading a profile mid-match reset
the board. They depend on the id now. The same effect also reset the store
while its fetch was in flight, so init() then wrote the match back into what
had just been cleared; it aborts before resetting.

Page reads take an AbortSignal and their effects abort on unmount. Several
already had a `cancelled` flag, which stops the state write but lets the
request run to completion; the flag stays to guard the `finally` blocks.

Undo was fire-and-forget: rapid clicks put overlapping POSTs in flight
against a server that removes one link per call, and a failure was logged
while the local chain had already been shortened. Undos are serialised and
the server's chain - which it was already returning and the client was
discarding - is now authoritative.

The runtime API setting was substituted into a JS string literal by envsubst,
which does no escaping, so a value containing a quote ended the literal and
the remainder executed on every page load. It is escaped at render time
(verified under busybox with the production values and with an injection
payload, which comes out inert), and a malformed value is rejected on read.
Note this deliberately does not assert same origin, as the plan suggested:
the frontend and API are served from different subdomains in production, so
that assertion would break the deployment.

The Endless difficulty selector is removed rather than wired. Difficulty is
derived from cast analysis on the server and Endless builds its pairs in the
browser, so there was nothing to wire it to - it silently did nothing while
telling the player their choice mattered.

Verified in the running app: an invalid token clears and redirects to
/login?expired=1 without looping, while a wrong password shows "Invalid email
or password" on the form and stays put; sign-out clears the token, returns to
sign-in and leaves no trace of the previous user; Endless renders without the
control and keeps its real filters. No console errors beyond the 401s those
checks deliberately provoked.
2026-09-10 21:22:31 -07:00
2026-03-08 00:11:16 -08:00
2026-03-08 00:11:16 -08:00
2026-05-05 17:15:50 -07:00
2026-03-08 00:11:16 -08:00
2026-03-11 11:36:21 -07:00
2026-03-08 00:11:16 -08:00
2026-03-08 00:11:16 -08:00
2026-03-08 00:11:16 -08:00

React + TypeScript + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

React Compiler

The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see this documentation.

Expanding the ESLint configuration

If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      // Other configs...

      // Remove tseslint.configs.recommended and replace with this
      tseslint.configs.recommendedTypeChecked,
      // Alternatively, use this for stricter rules
      tseslint.configs.strictTypeChecked,
      // Optionally, add this for stylistic rules
      tseslint.configs.stylisticTypeChecked,

      // Other configs...
    ],
    languageOptions: {
      parserOptions: {
        project: ['./tsconfig.node.json', './tsconfig.app.json'],
        tsconfigRootDir: import.meta.dirname,
      },
      // other options...
    },
  },
])

You can also install eslint-plugin-react-x and eslint-plugin-react-dom for React-specific lint rules:

// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      // Other configs...
      // Enable lint rules for React
      reactX.configs['recommended-typescript'],
      // Enable lint rules for React DOM
      reactDom.configs.recommended,
    ],
    languageOptions: {
      parserOptions: {
        project: ['./tsconfig.node.json', './tsconfig.app.json'],
        tsconfigRootDir: import.meta.dirname,
      },
      // other options...
    },
  },
])
S
Description
No description provided
Readme MIT
4.1 MiB
0 Stars 1 Watchers 0 Forks
Languages
TypeScript 97.7%
CSS 0.8%
JavaScript 0.7%
Shell 0.4%
Dockerfile 0.2%
Other 0.2%