The Turnstile render effect had a stale-closure hazard: it captured
onToken/onExpire/onError props but didn't list them in the dependency
array (deps: scriptReady, siteKey, theme, size). On parent re-renders
the captured callbacks could go stale.
Wrap the three callback props with useEffectEvent so they always read
the latest props without invalidating the render effect. The render
effect still only re-runs when the script readiness or widget config
truly change.
useEffectEvent shipped in React 19.2 (already on the project) and
@types/react 19.2.x exports it.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
Run eslint --fix across src/ to clear ~1900 mechanical lint errors
surfaced by the @typescript-eslint v8 + react-hooks v7 + react-compiler
upgrade in the React 19 modernization PR.
Issues fixed automatically:
- brace-style (Allman): try/catch one-liners reformatted to multi-line
- indent: tab-vs-space and depth corrections
- semi: missing trailing semicolons
- no-trailing-spaces
No semantic changes. Remaining 701 errors are real-code issues
(set-state-in-effect, rules-of-hooks, no-unsafe-* type checks) that
need manual per-file review.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
Migrate all three inline forms in LoginView.tsx to React 19 Actions:
- Login form: handleLoginSubmit → loginAction(prevState, FormData) wrapped in
useActionState. Submit button extracted as <LoginSubmitButton/> reading
pending via useFormStatus, dropping the local `submitting` flag for the
login flow. Reads username/password/remember from FormData; rememberMe
checkbox now carries name="remember".
- Forgot form (inline): forgotAction wrapped in useActionState; awaits
parent's onSubmit so pending stays true through the parent fetch.
ForgotSubmitButton uses useFormStatus.
- Register credentials step: credentialsAction with useActionState; the
step transition (setStep('avatar')) happens inside the action after
pingServer + onCheckEmail.
- Register avatar step: avatarAction validates username, pings server,
checks availability, then awaits onSubmit. The button label uses
isAvatarPending to show "Creating…" without prop drilling submitting.
- DialogSharedProps onSubmit signatures updated to return Promise<void>
so dialog actions can await the parent's fetch.
- lockState memo replaced with a direct readLock() call in render: the
previous useMemo depended on `submitting` to refresh after a failed
attempt; now any re-render (triggered by the action's pending toggle)
recomputes it.
- Remove unused FormEvent import; remove unused checking state in
RegisterDialog (replaced by isCredentialsPending / isAvatarPending).
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
Adopt React 19 idioms across the codebase. The runtime was already on
react@19.2.5 but no React 19 APIs were in use.
- forwardRef -> ref-as-prop in 7 layout/component files
(NitroInput/Button/ItemCountBadge/Card×5/InfiniteGridItem,
ToolbarItemView, AvatarEditorIcon)
- <Ctx.Provider> -> <Ctx> in 6 contexts (CatalogAdmin, FloorplanEditor,
UiSettings, GridContext, NitroCardContext, NitroCardAccordionContext)
- Native <script> hoisting for Turnstile, ExternalPluginLoader, GoogleAdsView
(React 19 dedupes by src; removes manual document.head.appendChild +
module-level promise caches)
- React Compiler enabled at build time via babel-plugin-react-compiler
in vite.config.mjs (target: '19'), plus eslint-plugin-react-compiler
in lint mode
- Global <ErrorBoundary> + <Suspense> in src/index.tsx using
react-error-boundary, with LoadingView as fallback
- BackgroundsView migrated to use(promise) as a demonstrator pattern
for Suspense-driven config loading
- ESLint react setting bumped 18.3.1 -> 19.2; legacy
@typescript-eslint/ban-types replaced with no-restricted-types
(the old rule was removed in @typescript-eslint v8)
- Refresh public/configuration/{asset-loader,bootstrap}.js to match
current write-asset-loader.mjs output
Phase 3 (login forms -> useActionState/useFormStatus) deferred:
LoginView is 1623 lines with lockout + Turnstile + heartbeat
interleaving; safer as its own PR.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
The example data has been provided in /Content-Gamedata so you could place it in /gamadata or anything you like.
Do not forget the render-config.json to update :
"login.health.method": "GET",
"login.news.url": "${asset.url}/news/news.json",
Adds a "Cards" tab to the Profile Background picker (BackgroundsView)
that selects a pattern applied to the entire user info card and the
extended profile container, in addition to the existing avatar-pad
background/stand/overlay layers.
- AvatarInfoUser/Utilities: propagate cardBackgroundId from RoomUserData.
- InfoStandWidgetUserView: stateful cardBackgroundId, applied as
.profile-card-background.card-background-{id} on the outer Column
with bg-color suppressed when active.
- UserContainerView: same class on the wrapper of the extended profile.
- BackgroundsView: 4th tab "cards" backed by cards.data config
(falls back to backgrounds.data); sends 4-id message via the
extended sendBackgroundMessage signature.
- ui-config.example: cards.data dataset (15 entries).
- BackgroundsView.css: 188 .card-background-{N} rules cloned from
background-{N} (repeat-tiled) plus 15 CSS-pattern overrides for the
provisional dataset (gradients, stripes, dots, grid, checker).
The face avatar (headOnly LayoutAvatarImageView) sits in a 63px-tall
box (44px on mobile) while sibling toolbar icons are smaller, so its
head sprite rendered visually higher than the other icons. Bumped
marginTop from 2px → 12px (desktop) and 4px → 9px (mobile) so the
head sits on the same horizontal axis as the rest of the toolbar.
Removed `absolute bottom-[60px] left-[33px]` from the inner Flex of
ToolbarMeView. The outer wrapper in ToolbarView already anchors the
popup above the face button (bottom-[calc(100%+8px)] left-1/2 -translate-x-1/2),
so the inner pixel-perfect override was detaching it and making it float
mid-screen.
Backend (AuthHttpHandler):
- New users_remember_tokens table stores sha256 hex of the raw token
so the DB never holds a usable credential. Seed file adds the table
and a login.remember.duration.days setting (default 30).
- /api/auth/login accepts "remember": true. On success, issues a fresh
32-byte base64url token, stores the hash, returns the raw token.
- New POST /api/auth/remember: accepts the raw token, looks up by hash,
on a valid hit mints a fresh SSO ticket, rotates the token (deletes
the consumed one and issues a new one), returns both to the client.
No Turnstile - it's an automated trusted-device flow.
- /api/auth/logout also accepts rememberToken and deletes that single
row so other devices keep their tokens.
Frontend:
- LoginView: "Remember me" checkbox (key login.remember_me already in
ExternalTexts). Enabling it persists the returned rememberToken in
localStorage.nitro.remember.token.
- App.tsx: before deciding to show the login screen, try a silent POST
to /api/auth/remember with the stored token. On 200, inject the
returned ssoTicket into window.NitroConfig and proceed to the
authenticated flow; on 401, forget the token and show login.
- PurseView logout: sends the stored rememberToken in the body so the
server can delete it, and clears localStorage before reload.