Commit Graph

432 Commits

Author SHA1 Message Date
simoleo89 f75762a2db Add CLAUDE.md + refresh docs/ARCHITECTURE.md to current state
Two doc changes so a fresh local Claude Code session can pick up the
branch without re-discovering the conventions and the work-in-progress.

CLAUDE.md (new, repo root)
- Onboarding file Claude Code reads automatically at session start.
- TL;DR with branch name + PR number, points at docs/ARCHITECTURE.md.
- Stack snapshot (React 19, TS 7 native, Vite 8 + Compiler, Zustand 5,
  TanStack Query 5, Vitest 3).
- Layout convention spelled out — `src/components/<area>/<feature>/`
  for views, `src/hooks/<area>/<feature?>/` flat for hooks. The
  rejected-feature-folders decision is the most stepped-on rake, so
  it lives here at the top.
- The canonical 3-file god-hook split shape with doorbell as the
  reference.
- Patterns to use with copy-pasteable signatures: useNitroEventState,
  useMessageEventState, useNitroQuery (with the accept() filter),
  Zustand stores via createNitroStore, WidgetErrorBoundary.
- "Wired up vs not yet" matrix: what each pattern is adopted on and
  what the next reasonable target is.
- Pointer to the two still-open logic bugs (MainView CREATED/ENDED
  race; LayoutFurniImageView async fetch race) with fix shapes.
- House rules: commit author override, no claude/... branch names,
  never merge a layout-violating branch, skip-motivated splits are
  fine if explained in the commit message.

docs/ARCHITECTURE.md (refresh)
- "What's already in place" rewritten to reflect the full state of
  the feat/react19-modernization branch:
    * stale references to the old claude/update-react-typescript-He2rs
      branch removed
    * the three additional god-hook splits done since the last edit
      (furni-chooser, user-chooser, friend-request) added
    * the 4 useNitroQuery migration sites listed (OfferView,
      CatalogLayoutRoomAdsView, ModToolsChatlogView, CfhChatlogView)
    * the three additional WiredCreatorToolsView tab extractions
      (Monitor, Inspection, Variables) with the 4493 -> 3544 line
      counter
    * dead-code removal of the legacy login dialogs documented
    * the Vitest count updated from 22 to 77 across 6 test files
    * usePollSubscriptions hoist to RoomWidgetsView noted
- "How to pick the next refactor PR" rewritten:
    * completed items removed (the previous list still had
      "hoist usePollSubscriptions" as todo even though it's done,
      and "per-tab WiredCreatorTools split" same)
    * remaining priorities re-ordered: useCatalog migration (1),
      useCatalog split (2), per-widget error boundaries (3),
      wired-tools shared-state Zustand slice (4), the two open
      logic bugs (5), wider Vitest coverage (6).
    * "skipped intentionally" subsection added for the god-hook
      splits that need design work first (pet-package, word-quiz,
      chat-input, chat-widget, avatar-info).

Verification
- yarn test: 77/77 still passing.
- grep claude/update-react-typescript-He2rs docs/ARCHITECTURE.md: 0
  (no stale branch refs).

Now a fresh `claude` session in this repo can read CLAUDE.md, follow
the link to ARCHITECTURE.md, and start contributing without re-asking
the conventions.
2026-05-11 18:05:12 +00:00
simoleo89 dbafc97e89 Drop unused login dialogs (dead code) + Vitest coverage on FriendlyTime
Two unrelated cleanups grouped because they're both small and safe.

Dead code removal
- src/components/login/components/RegisterDialog.tsx
- src/components/login/components/ForgotDialog.tsx
- src/components/login/components/shared.ts (only consumed by the two
  dialogs above)

These were the older non-Form-Actions versions of the register and
forgot-password dialogs. LoginView.tsx defines its own inline versions
that use `useActionState` + `useFormStatus` (Phase 3 of the React 19
modernization), which are the ones actually rendered. The legacy
files were already documented as dead in docs/ARCHITECTURE.md.

NewsWindow.tsx and the `components/` directory itself stay — NewsWindow
is still imported by LoginView at the bottom of the login flow.

Vitest coverage on FriendlyTime (+12 cases)
- 65 -> 77 passing tests, 5 -> 6 test files.
- LocalizeText is mocked with a deterministic stub
  (`${ key }|${ amount }`) so each assertion can verify both the bucket
  chosen and the rounded amount. The mock also short-circuits the
  transitive renderer-SDK import, which keeps the test runner
  decoupled from the renderer install state.
- Buckets covered: seconds / minutes / hours / days / months / years
  for both `format` and `shortFormat`. Plus: threshold override,
  key-suffix concatenation, half-hour rounding, the raw
  `getLocalization` helper.

Verification
- yarn test: 6 files / 77 cases / ~2s.
- yarn eslint on the new test file: 0 errors / 0 warnings.
- yarn tsc: clean on touched files.
2026-05-11 17:59:46 +00:00
simoleo89 bb28d252d8 Vitest: +16 cases on ColorUtils, FixedSizeStack, LocalizeFormattedNumber
49 -> 65 passing tests, 4 -> 5 test files.

New file: tests/api-utils-extra.test.ts (16 cases)
- LocalizeFormattedNumber (3): zero/NaN/null guard, sub-1000 stays,
  >=1000 inserts thin-space group separators.
- ColorUtils (8): makeColorHex, makeColorNumberHex (with zero-pad),
  convertFromHex (with/without #), int_to_8BitVals/eight_bitVals_to_int
  roundtrip, int2rgb pure-RGB output, zero-input edge cases.
- FixedSizeStack (4): grow then overwrite oldest (ring-buffer
  semantics), reset clears state, partial-fill behavior of getMax,
  empty-stack returns Number.MIN_VALUE. The "partial-fill" case
  documents a subtle quirk: getMax iterates the whole maxSize window
  including undefined slots, but `undefined > X` is false in JS so
  the inserted value wins — the test pins that behavior.

Note on `usePetPackageWidget` and `useWordQuizWidget`
- They were both considered for a state/actions split this turn but
  their actions mutate internal state (`onClose` resets 5 useState,
  `vote` reads pollId/question/answerSent). A clean split would
  require either passing args to the action or hoisting the state
  to a shared store first. Deferred as follow-up.

Verification
- yarn test: 5 files / 65 cases / ~1.9s.
- yarn eslint on the new test file: 0 errors / 0 warnings.
2026-05-11 17:50:47 +00:00
simoleo89 f3442f8aa0 Split useFriendRequestWidget into state + actions (flat hooks layout)
Stesso pattern di doorbell / poll / furni-chooser / user-chooser:
flat split sotto src/hooks/rooms/widgets/, no co-location dentro
src/components/.

Split
- src/hooks/rooms/widgets/useFriendRequestState.ts (new):
  activeRequests state + displayedRequests derived (filter su
  dismissedRequestIds) + due bridge events (user added/removed) +
  un useEffect che riallinea activeRequests quando cambia il set
  di requests dal friends-store. Esporta anche il tipo
  ActiveFriendRequest per consumi futuri.
  Plus: ?. su roomSession e userDataManager per evitare il bug
  pattern "session è null in transition" (vedi PetTrainingPanel,
  precedentemente fixato).
- src/hooks/rooms/widgets/useFriendRequestActions.ts (new):
  hideFriendRequest. Thin adapter sul friends-store
  (setDismissedRequestIds), nessuna subscription.
- src/hooks/rooms/widgets/useFriendRequestWidget.ts: deprecated
  shim che compone i due e preserva
  { displayedRequests, hideFriendRequest } per il consumer
  FriendRequestWidgetView.

Verifica
- yarn eslint sui 4 file toccati: 1 errore pre-esistente
  (set-state-in-effect sul useEffect che ri-derive activeRequests
  da requests — già nel file originale, baseline invariata).
- yarn test: 49/49 passing.
- yarn tsc: clean.

Sequence widget split adesso a 5 (doorbell, poll, furni-chooser,
user-chooser, friend-request). Rimangono: usePetPackageWidget,
useWordQuizWidget, useChatInputWidget, useChatWidget,
useAvatarInfoWidget, useFilterWordsWidget.
2026-05-11 17:47:09 +00:00
simoleo89 85fc82794d Split useUserChooserWidget into state + actions (flat hooks layout)
Speculare di useFurniChooserWidget — stesso split + stesso layout (flat
in src/hooks/rooms/widgets/). User chooser è il gemello del furni
chooser nella shape: items list popolata da room scan + due bridge
events (added/removed) + selectItem imperativo.

Split
- src/hooks/rooms/widgets/useUserChooserState.ts (new):
    items + onClose + populateChooser + useUserAddedEvent +
    useUserRemovedEvent. Helper buildUserItem dedupa la costruzione
    di RoomObjectItem fra populateChooser e l'add handler (~20
    righe di duplicazione in meno).
    Plus: aggiunto ?. su roomSession e userDataManager (lo stesso
    bug pattern del PetTrainingPanel fixato altrove).
- src/hooks/rooms/widgets/useUserChooserActions.ts (new):
    selectItem puro.
- src/hooks/rooms/widgets/useUserChooserWidget.ts: kept as a
    deprecated shim that composes both and preserves
    { items, onClose, selectItem, populateChooser } per il consumer
    UserChooserWidgetView.

Verifica
- yarn eslint sui 4 file toccati: 0 errors / 0 warnings.
- yarn test: 49/49 passing.
- yarn tsc: clean.

Sequenza god-hook split adesso a 4 (doorbell, poll, furni-chooser,
user-chooser). Rimangono: useFriendRequestWidget, usePetPackageWidget,
useWordQuizWidget, useChatInputWidget, useChatWidget,
useAvatarInfoWidget, useFilterWordsWidget.
2026-05-11 17:45:31 +00:00
simoleo89 0ae371ee09 Split useFurniChooserWidget into state + actions (flat hooks layout)
Apply the same data/actions split pattern (proposal #4) to
useFurniChooserWidget, the largest god-hook still on the widgets
side (161 LOC). Layout follows the main branch convention:
flat files under src/hooks/rooms/widgets/, no per-feature subfolder,
no co-location of hooks inside src/components/.

Split
- src/hooks/rooms/widgets/useFurniChooserState.ts (new): owns the
  items array, the populateChooser action that scans the current
  room, the two RoomEngine event bridges (added/removed), and
  onClose. Helper buildWallItem/buildFloorItem dedupes the two
  copies of the RoomObjectItem construction that used to live
  inline in both populateChooser and the added-event handler
  (~50 lines of duplication removed).
- src/hooks/rooms/widgets/useFurniChooserActions.ts (new): the
  one pure imperative action — selectItem — that doesn't need to
  subscribe to anything.
- src/hooks/rooms/widgets/useFurniChooserWidget.ts: kept as a
  deprecated shim that composes both and returns the same
  { items, onClose, selectItem, populateChooser } shape so
  FurniChooserWidgetView (the only consumer) doesn't change.

Layout note
- This is consistent with the main branch: each widget hook is a
  flat file under src/hooks/rooms/widgets/ (no <feature>/ subfolder),
  while the view sits under src/components/room/widgets/<feature>/.
- The parallel feat/react19-hooks-adapter branch chose the opposite
  convention (hooks co-located inside src/components/...). Per the
  team decision recorded in docs/ARCHITECTURE.md proposal #3, this
  repo stays on the flat-hooks-folder layout.

Verification
- yarn tsc on the touched files: 6 TS2347 errors after the split,
  12 before — the buildWallItem/buildFloorItem helpers actually
  *reduce* the local sandbox TS2347 surface (the renderer SDK is
  not installed locally, so `roomObject.model.getValue<T>` is
  flagged as "untyped function with type arg"; merging the two
  callsites into one helper halves the count).
- yarn eslint on the touched files: 0 errors, 0 warnings.
- yarn test: 49/49 passing.
2026-05-11 17:09:41 +00:00
simoleo89 bf84a0c2a6 useNitroQuery: add accept() predicate; migrate two mod-tools chatlog views
Many composer/parser pairs on the Nitro wire are correlation-key based:
the request carries a key (roomId, issueId, etc.) and the response shows
up on the globally-shared event bus, where other components may be
listening for the same parser type with a different key. The previous
useNitroQuery resolved on the FIRST matching parser event regardless of
key — useless for that pattern, which is why two obvious migration
targets (ModToolsChatlogView, CfhChatlogView) were skipped earlier.

Adapter change
- New optional `accept?: (event) => boolean` on NitroQueryConfig.
- In awaitNitroResponse, events for which accept returns false are
  IGNORED rather than resolving the promise. The listener stays
  registered, the timeout still applies. This lets callers do:
    accept: e => e.getParser()?.data.roomId === roomId

Migrations
- src/components/mod-tools/views/room/ModToolsChatlogView.tsx
  - Was: useState<ChatRecordData>(null) + useMessageEvent with
    `if (parser.data.roomId !== roomId) return; setRoomChatlog(...)` +
    a mount-only useEffect dispatching the composer.
  - Now: a single useNitroQuery call keyed on roomId; accept filters
    by roomId; the query is enabled only when roomId is set.
    The composer is no longer re-dispatched on remount within
    staleTime; switching to a different room still triggers a fresh
    fetch because the queryKey changes.
- src/components/mod-tools/views/tickets/CfhChatlogView.tsx
  - Same pattern, keyed on issueId.

Both migrations drop ~15 lines per file (no more local state + manual
listener + manual send) while gaining cache/dedup/loading/error
handling from TanStack Query.

Verification
- yarn eslint on the four files: 1 pre-existing error (the
  IMessageEvent "redundant union" false positive in createNitroQuery
  that we already documented — local sandbox doesn't have the
  renderer SDK installed, so its types resolve as `any`).
- yarn test: 49/49 passing.
- yarn tsc on the four files: clean.
2026-05-11 17:00:06 +00:00
simoleo89 bb09a562f6 Extract Monitor tab JSX into WiredMonitorTabView + drop dead overlays
Third (and final, for now) inline-tab extraction in WiredCreatorToolsView.
With this commit Monitor / Inspection / Variables / Settings are all
sibling components; the parent only orchestrates state.

What moved
- ~60 lines of live JSX (Statistics card, Logs table, "Clear all" +
  "View full logs" buttons) → src/components/wired-tools/WiredMonitorTabView.tsx
- The new component takes 7 typed props (3 data + 4 callbacks), no
  state or effects.

Dead code removed
- The Monitor block also contained three modal-style overlays
  (History / Info / Error info) wrapped in `{ false && ... }` — they
  never rendered. The live versions of those modals are mounted by
  the parent outside the NitroCardView (lines ~3327, ~3393, ~3679 in
  the new layout). Dropping the dead duplicates removes ~115 lines
  and ten otherwise-unused symbol references from the parent.

Impact
- WiredCreatorToolsView.tsx: 3710 → 3544 lines (−166 net).
  Combined with the previous two extractions and the
  types/constants/helpers split in 3c68d97, the file is now down
  from 4493 → 3544 lines (−949, −21%).
- The three tab files are each ~150 lines and trivially scannable.

Conscious non-goals
- No state hoisted to a store yet. The shared-state Zustand slice
  is a separate PR. This commit only relocates JSX.
- Behavior unchanged for live code paths. Removing the
  `{ false && ... }` overlays cannot change behavior because they
  were dead branches; the live overlays at the bottom of the parent
  module are the ones the user actually sees.

Verification
- yarn eslint on the two files: 34 problems baseline, 34 after
  (no new issues introduced).
- yarn test: 49/49 passing.
- yarn tsc on the touched files: clean.
2026-05-11 16:56:56 +00:00
simoleo89 d7d9a7e382 Extract Inspection tab JSX into WiredInspectionTabView component
Second of three slices to break up the WiredCreatorToolsView inline
tab bodies (Variables tab was split in the previous commit; Monitor
remains).

What moved
- 139 lines of inline JSX (`{ activeTab === 'inspection' && <div>
  ... </div> }`) → src/components/wired-tools/WiredInspectionTabView.tsx
- The new component declares 28 typed props grouped by area:
  element-type + preview, keep-selected toggle, variables table,
  inline editor, give-variable popover, remove variable. All state
  and actions arrive from the parent — no internal useState/useEffect.
- The "select variable + start editing" double action at the parent
  is wrapped into a single onSelectInspectionVariable callback so
  the sub-component doesn't need to know about the two setters.
- The renderer-SDK type IWired*VariableDefinition is replaced by a
  structural InspectionGiveDefinition declared in the view file:
  { itemId, name, hasValue }. Keeps the sub-component free of
  renderer-SDK imports.

Impact
- WiredCreatorToolsView.tsx: 3809 → 3710 lines (−99 net). Combined
  with the previous commit, the file is now down 191 lines from the
  4493-line single-monolith it was 6 commits ago.
- Inspection panel JSX is now visually scannable as a file. The
  parent only orchestrates state and passes it down.

Conscious non-goals
- No state hoisted. selectedInspectionVariableKeys, editingVariable,
  isInspectionGiveOpen, inspectionGiveValue etc. all still live in
  the parent useState. The Zustand slice for shared wired-tools state
  is a follow-up PR.
- No behavior change. Same renders, same handlers, same DOM.

Verification
- yarn eslint on the two files: 34 problems baseline, 34 after split
  (the same pre-existing FC<{}> + 5 set-state-in-effect on the parent
  module + react-compiler skip warnings).
- yarn test: 49/49 passing.
- yarn tsc on the two files: clean.

Next: extract the Monitor tab (~176 lines), the last inline tab body.
2026-05-11 16:53:52 +00:00
simoleo89 23fc302b24 Extract Variables tab JSX into WiredVariablesTabView component
Proposal #5 from docs/ARCHITECTURE.md, first slice: split one of the
three remaining inline tab bodies of WiredCreatorToolsView out into
its own file. Same approach the Settings tab has had for a while
(see WiredToolsSettingsTabView).

What moved
- 113 lines of inline JSX (the `{ activeTab === 'variables' && <div>
  ... </div> }` block) → src/components/wired-tools/WiredVariablesTabView.tsx
- The new component is a pure presentation function: 12 typed props,
  no useState, no useEffect, no event subscriptions. It receives:
    * state to render: variablesType, variablePickerDefinitions,
      selectedVariableDefinition, canVariableHighlight,
      isVariableHighlightActive, variableManageCanOpen,
      selectedVariableProperties, selectedVariableTextValues
    * actions to call: onVariablesTypeChange, onPickVariable,
      onToggleVariableHighlight, onOpenManagePanel
- The parent supplies all of them inline at the call site. The
  manage-panel open sequence (request fresh user vars + reset page +
  clear selection + show modal) is closed over into a single
  onOpenManagePanel callback, so the sub-component doesn't need to
  know about its three internal setters.

Impact
- WiredCreatorToolsView.tsx: 3901 → 3809 lines (−92 net). The file
  is still large, but one of the three big inline blocks is gone.
  Monitor (~176 lines) and Inspection (~138 lines) remain inline as
  follow-up PRs.
- The React Compiler now has a smaller file boundary for the
  Variables panel; once the other two blocks come out the parent
  module should stop being skipped for memoization.

Conscious non-goals
- No state was moved. The shared state (selectedVariableKeys,
  isVariableHighlightActive, variableManagePage, etc.) still lives
  in the parent's useState. Hoisting them to a Zustand slice would
  be a separate PR — premature here.
- No behavior change. Same renders, same handlers, same DOM.

Verification
- yarn eslint on the two touched files: 34 problems baseline,
  34 problems after the split (identical: same FC<{}>, same
  pre-existing set-state-in-effect, same react-compiler skip
  warnings on the parent module).
- yarn test: 49/49 passing.
- yarn tsc on the two files: clean.
2026-05-11 16:46:48 +00:00
simoleo89 388fb8ed34 Migrate CatalogLayoutRoomAdsView's room-ad fetch to useNitroQuery
Second concrete adoption of proposal #2 (first was OfferView).

Before
- A useState<RoomEntryData[]>([]) for availableRooms.
- A useMessageEvent<RoomAdPurchaseInfoEvent> handler that
  set the state on each parser event.
- A useEffect on mount that dispatched two composers, one of which
  was GetRoomAdPurchaseInfoComposer paired with the parser above.

After
- A single useNitroQuery call wires the request and parser as one
  read-only query. The select extracts parser.rooms with a default
  empty array.
- staleTime is 60s — opening the same panel within a minute reuses
  the cached value; the composer is not re-dispatched. Useful here
  because the user navigates between catalog tabs.
- The mount-only useEffect no longer dispatches the room-ad composer;
  the second composer (GetUserEventCatsMessageComposer) stays where
  it was — that one feeds useNavigator state and isn't a
  request-response pair this component owns.

Why this file
- It was the cleanest pattern in the catalog tree: no correlation
  keys, no conditional filter on the parser, no other writes to
  availableRooms. The pure derive-from-event case useNitroQuery is
  built for.
- The big god-hook useCatalog (1100 LOC) still owns most of the
  catalog data layer; migrating that needs the data/uiState/actions
  split first.

Verification
- yarn test: 49/49 still passing.
- yarn eslint on the touched file: 1 error (the pre-existing
  set-state-in-effect on line 36, unchanged — baseline matches).
- The previous useMessageEvent import was removed cleanly.
2026-05-11 16:40:43 +00:00
simoleo89 9d2e4a7324 Expand Vitest coverage on the pure helpers in src/api/{utils,wired}
22 -> 49 passing tests, 2 -> 3 test files.

Targets are functions with zero external dependencies (no renderer SDK,
no network, no DOM). They were picked because:
- they're easy to break by accident in a refactor (rounding edge cases,
  zero-padding rules);
- their behavior is documented by tests once and for all, including the
  surprising bit about LocalizeShortNumber rounding 950..999 into the
  "1K" bucket (kept as an explicit "documented quirk" assertion rather
  than fixed — the current behavior is what the rest of the app
  expects).

New file: tests/api-utils.test.ts (27 cases)
- ConvertSeconds: zero, 1m / 1h / 1d, mixed, single-digit padding (6).
- LocalizeShortNumber: zero/NaN/null guard, sub-1000 stays as-is, K/M/B
  buckets, negative numbers, the 950..999 rounding quirk (7).
- CloneObject: primitives, identity preservation, key fidelity (3).
- GetWiredTimeLocale: even (whole sec), odd (half sec), zero (3).
- WiredDateToString: zero-pad rules, two-digit values (2).
- PrefixUtils.parsePrefixColors: empty inputs, mapping, color reuse (3).
- PrefixUtils.getPrefixFontStyle: default empty id, known preset,
  unknown id (3).

Verification
- yarn test: 3 files / 49 cases / ~1.1s.
- yarn eslint on tests/: 0 errors / 0 warnings.
- All test targets are stable pure functions; the assertions
  double as documentation for callers.
2026-05-11 16:38:12 +00:00
simoleo89 419de09638 Hoist usePollSubscriptions to RoomWidgetsView; drop the side effect from usePollWidget
Follow-up to the previous commit's poll split. The compat shim
usePollWidget used to call usePollSubscriptions() inside its body so
the three RoomSessionPollEvent listeners were still registered for
existing consumers — but that meant:
- listeners would be re-registered per consumer (today nobody, since
  useWordQuizWidget was already migrated to usePollActions);
- the lifetime of the subscriptions was tied to a leaf widget instead
  of the room session;
- a render of a component using the shim had the side effect of
  attaching three global event listeners.

Move
- src/components/room/widgets/RoomWidgetsView.tsx now calls
  usePollSubscriptions() once at the top of the room-widget tree. The
  bridge from RoomSessionPollEvent (OFFER/ERROR/CONTENT) to the UI
  event bus is now mounted for exactly the lifetime of an in-room
  session, regardless of which leaf widget renders.
- src/hooks/rooms/widgets/usePollWidget.ts (compat shim) is reduced
  to a one-liner that just returns usePollActions(). It is still
  deprecated; remove once nothing imports it.

Verification
- yarn eslint on the two touched files: 1 pre-existing error
  (the same FC<{}> in RoomWidgetsView that was there before — baseline
  unchanged; I deliberately did not touch it in this commit to keep
  the diff minimal).
- yarn test: 22/22 still passing.
- grep confirms usePollWidget has zero in-tree consumers; the only
  importer is the barrel re-export.
2026-05-11 16:36:11 +00:00
simoleo89 7218285583 Split usePollWidget into subscriptions + actions (proposal #4) + doc update
usePollWidget bundled two unrelated responsibilities:
- three useNitroEvent listeners that bridge RoomSessionPollEvent
  (OFFER / ERROR / CONTENT) onto the UI event bus via DispatchUiEvent
  — pure side-effects, zero local state, should mount once;
- three imperative actions (startPoll, rejectPoll, answerPoll) that
  every consumer wants, but which shouldn't re-register the listeners.

In practice the only consumer of usePollWidget was useWordQuizWidget,
which needed only `answerPoll` — but pulled in the three subscriptions
as a side effect every time the word-quiz widget rendered. That's the
classic god-hook anti-pattern this proposal targets.

Split (mirrors the doorbell pattern already in place):
- src/hooks/rooms/widgets/usePollSubscriptions.ts (new): the three
  bridge listeners, returns void. Should be mounted ONCE at the
  highest stable level above poll-aware UI (room widgets root). For
  now still mounted by the shim — follow-up PR can move it.
- src/hooks/rooms/widgets/usePollActions.ts (new): the three
  imperative actions. Defensive `?.` on roomSession so a poll action
  during a room transition no longer crashes.
- src/hooks/rooms/widgets/usePollWidget.ts: kept as a deprecated shim
  that composes both — preserves the old `{ startPoll, rejectPoll,
  answerPoll }` shape so existing consumers don't break.
- src/hooks/rooms/widgets/useWordQuizWidget.ts: migrated to import
  usePollActions directly. The word-quiz widget no longer registers
  poll subscriptions transitively — its render no longer has the side
  effect of subscribing to three renderer events.

Doc
- docs/ARCHITECTURE.md "What's already in place": records both god-hook
  splits (doorbell + poll), the now-enabled React Query and Zustand,
  and the test infrastructure. Removes the "not yet enabled" markers
  for #2 and #5.
- "How to pick the next refactor PR": rewritten to reflect that the
  foundations are done. New priority order:
    1. migrate useCatalog's read-only fetches to useNitroQuery,
    2. hoist usePollSubscriptions to room-session level,
    3. split useCatalog along the doorbell/poll lines,
    4. broaden Vitest coverage,
    5. per-tab WiredCreatorToolsView split.

Verification
- yarn eslint on the touched files: 0 errors / 0 warnings.
- yarn test: 22/22 passing, 2 files, ~1.0s.
- Existing useWordQuizWidget consumers (RoomWidgetsView ->
  WordQuizWidgetView) unaffected — they import from the barrel which
  still re-exports the same shape.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:53 +00:00
simoleo89 6793de2106 Set up Vitest + 22 smoke tests on pure modules (proposal #6)
Phase 3 of the refactor plan in docs/ARCHITECTURE.md — the foundation
that unblocks every safe refactor below.

Install
- yarn add -D vitest@3 jsdom @testing-library/dom @testing-library/react
  @testing-library/jest-dom

Note: pinned to vitest@3 (not the latest 4.x) because yarn 1's peer
resolution breaks on vitest@4's peer link to vite. With vitest@3 the
existing Vite 8 install resolves cleanly.

Configuration
- vitest.config.mts (new): separate from vite.config.mjs because the
  dev/build config wires up renderer SDK aliases that point at sibling
  working trees (../renderer, ../Nitro_Render_V3). Tests are written
  against pure modules that don't pull in the renderer, so the test
  runner uses a smaller alias set.
- tests/setup.ts (new): imports @testing-library/jest-dom/vitest so
  custom matchers (toBeInTheDocument, etc.) are available without
  per-file imports.
- tsconfig.json: include "tests" so eslint stops complaining about
  unparseable files; also makes the IDE see the test files.
- package.json scripts: "test" (one-shot) and "test:watch".

Tests
- tests/WiredCreatorTools.helpers.test.ts (18 cases): covers the pure
  helpers extracted in 3c68d97 — createEmptyMonitorSnapshot,
  formatMonitorLatestOccurrence (5 time-bucket branches),
  formatMonitorHistoryOccurrence, formatVariableTimestamp,
  formatMonitorSource (4 branches), normalizeMonitorReason. These are
  the most boring-but-easy-to-break functions; locking them down first
  is high value, near-zero risk.
- tests/navigatorRoomCreatorStore.test.ts (4 cases): exercises the
  Zustand store added in the previous commit — initial state, latch
  semantics, 5s auto-reset (with fake timers), and the
  "second beginCreate restarts the lockout" invariant. Validates that
  the store-based replacement of the let-singleton has the same
  observable behavior, plus the new invariant that wasn't possible
  before (timer composition under StrictMode double-mount).

Side effect: two non-test source files were converted to `import type`
to keep the test bundle from accidentally pulling in the renderer SDK
transitively:
- src/components/wired-tools/WiredCreatorTools.types.ts
  (`import type { AvatarInfoFurni }`)
- src/components/wired-tools/WiredCreatorTools.helpers.ts
  (`import type { HotelDateTimeParts, MonitorSnapshot }`)
This is harmless — TypeScript already treated them as type-only —
and improves tree-shaking on build as a side benefit.

Verification
- yarn test -> 2 files, 22 tests passing in ~1.0s.
- yarn eslint on tests/ + the two type-only-import files: 0 errors,
  0 warnings.

Migration path
- Next adoption targets: cover useDoorbellState reducer (data hook
  split), the new useNitroQuery adapter (timeout/cleanup behavior),
  and the smaller pure formatters under src/api/.
- React component tests (via @testing-library/react) deferred until
  there's a small mock layer for the renderer SDK. The
  @testing-library/* deps are already installed so that PR is
  unblocked.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:53 +00:00
simoleo89 fd1835ca5d Enable Zustand (proposal #5) + convert isCreatingRoom singleton
Phase 2 of the refactor plan in docs/ARCHITECTURE.md.

Install
- yarn add zustand (^5, matches React 19 peer requirement).

Wiring
- src/state/createNitroStore.ts: replaces the previous prototype
  (which threw on call) with a re-export of zustand's `create` under
  the project-local name `createNitroStore`. Comments document the
  convention (one store per domain, subscribe to slices not the whole
  store).

First migration target
- src/components/navigator/views/navigatorRoomCreatorStore.ts (new):
  a Zustand store with `isCreating: boolean` and `beginCreate()` —
  the latter latches the flag to true, dispatches an internal
  setTimeout to auto-reset after 5s, and replaces any in-flight timer
  on re-entry. The timer handle lives in the store's closure, so a
  remount of the view doesn't reset the lockout and StrictMode's
  double-mount no longer schedules two pending timers.
- src/components/navigator/views/NavigatorRoomCreatorView.tsx:
  removes the two module-level `let` variables that the React Compiler
  was flagging ("Writing to a variable defined outside a component is
  not allowed"). The component now reads `isCreating` via a slice
  subscription and calls `beginCreate()` from the click handler. The
  imperative guard (`if (isCreating) return`) uses
  `useRoomCreatorStore.getState()` so it reads the latest value
  synchronously without being a stale closure.
- Also cleans up `FC<{}>` -> `FC` while touching the file.

Verification
- yarn eslint on the three touched files: 1 pre-existing error
  (the `setCategory(categories[0].id)` set-state-in-effect on the
  categories hook, deliberately left as-is in Phase C — it's the
  "init from late-arriving async data" pattern; baseline matches).
- yarn tsc: clean.

Migration path (per docs/ARCHITECTURE.md)
- This is the smallest possible Zustand pilot (~30 lines), chosen
  because the let-singleton anti-pattern was the most obvious quick
  win and the React Compiler was already complaining about it.
- Next adoption targets (cross-feature UI state): the toolbar's
  active-window state (currently inside scattered Contexts), the
  notification center's open-state, the catalog's currentPage/selection
  state (after the god-hook split).

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:53 +00:00
simoleo89 34b1b56788 Enable React Query (proposal #2) + first real-data pilot on OfferView
Phase 1 of the refactor plan in docs/ARCHITECTURE.md.

Install
- yarn add @tanstack/react-query@5 @tanstack/react-query-devtools@5
- Both pinned to ^5 (matches React 19 peer requirement).

Wiring
- src/index.tsx: mounts QueryClientProvider above ErrorBoundary +
  Suspense. Default config: staleTime=30s, retry=1,
  refetchOnWindowFocus=false (chat client, not a data dashboard).

Adapter
- src/api/nitro-query/createNitroQuery.ts: replaces the previous
  prototype that just threw. Exposes:
    * useNitroQuery({ key, request, parser, select, timeoutMs })
      — wraps TanStack's useQuery; queryFn awaits the parser response.
    * awaitNitroResponse(...) — lower-level helper for imperative use
      via queryClient.fetchQuery.
  The Promise:
    1. registers the parser via GetCommunication().registerMessageEvent
    2. dispatches the composer via SendMessageComposer
    3. resolves with select(event) on the first matching parser
    4. rejects after timeoutMs (default 15s)
    5. always cleans up the listener + timeout (cancel-safe).

Pilot
- src/components/catalog/views/targeted-offer/OfferView.tsx:
  the previous useMessageEventState + manual useEffect-send pattern
  becomes a single useNitroQuery call. staleTime:Infinity because the
  targeted offer doesn't change during a session. Subsequent OfferView
  remounts (e.g. opening/closing the dialog) now reuse the cached
  payload — the GetTargetedOfferComposer is no longer re-sent each
  time.

Verification
- yarn eslint on the four touched files: 1 pre-existing
  no-redundant-type-constituents error (IMessageEvent resolves as `any`
  in the local sandbox without the renderer SDK installed; matches the
  12 other pre-existing instances of the same false positive).
- yarn tsc on the four touched files: clean (modulo the
  project-wide TS2307 about @nitrots/nitro-renderer).
- The original prototype's "throw" guard is gone — useNitroQuery is now
  callable.

Migration path (per docs/ARCHITECTURE.md)
- Next adoption targets (read-only fetches first): useCatalog's page
  data, useInventoryFurni's bot listing, Navigator search results,
  Marketplace listings.
- Push messages (server-pushed events the client doesn't request)
  keep using useNitroEventState / useMessageEventState — they're
  subscriptions, not requests.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:53 +00:00
simoleo89 0755285708 Revert feature-folder migration; keep classic src/components + src/hooks layout
Decision: the src/features/<feature>/ layout introduced as proposal #3
(pilot on doorbell in 8ec9d27) is not the convention the team wants. The
existing src/components/<area>/ + src/hooks/<area>/ split is the one
that stays.

What's reverted
- src/features/doorbell/ is removed entirely. The doorbell view and the
  two hooks move back under the classic paths:
    src/features/doorbell/views/DoorbellWidgetView.tsx
      -> src/components/room/widgets/doorbell/DoorbellWidgetView.tsx
    src/features/doorbell/hooks/useDoorbellState.ts
      -> src/hooks/rooms/widgets/useDoorbellState.ts
    src/features/doorbell/hooks/useDoorbellActions.ts
      -> src/hooks/rooms/widgets/useDoorbellActions.ts
- The compat shims that lived in those classic paths are dropped now
  that the real files are back.
- src/hooks/rooms/widgets/index.ts adds the two new hooks alongside the
  existing useDoorbellWidget shim (kept as a deprecated wrapper so any
  external consumer importing the old shape via the barrel keeps
  working).

What's preserved
- The split between data and actions (proposal #4) — useDoorbellState
  and useDoorbellActions remain two separate hooks. This was the actual
  improvement, and it's independent of where the files sit.
- The bug fixes from 8ec9d27 (close button race, optimistic-remove
  rollback) — both still present, just in the new path.
- src/state/createNitroStore.ts and src/api/nitro-query/createNitroQuery.ts
  are left where they are. They aren't feature folders; they're
  cross-cutting framework code (Zustand skeleton, React Query adapter
  prototype) that any feature can consume.

Doc
- docs/ARCHITECTURE.md section #3 is rewritten to record the decision
  rather than recommend the layout. It now describes the convention to
  follow:
    * views under src/components/<area>/<feature>/
    * hooks under src/hooks/<area>/<feature?>/ (siblings, not subfolders
      per widget)
    * sibling .types/.constants/.helpers files for view-specific code
      (e.g. WiredCreatorTools.*.ts)
- "What's already in place" and "Recently fixed" sections updated to
  point at the new paths.
- "How to pick the next refactor PR" no longer mentions feature-folder
  migration as an option.

Note: the five extra feature folders started this session (reconnect,
nitropedia, ads, hc-center, campaign) were never committed; they only
existed in the working tree and have been restored from HEAD.

Verification
- find src/features -type f -> 0 (directory removed).
- npx tsc --noEmit on all touched files: clean (only the project-wide
  pre-existing TS2307 about @nitrots/nitro-renderer not installed
  locally remains, same as before).
- npx eslint on all touched files: 0 errors, 0 warnings.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:53 +00:00
simoleo89 81656e7b19 Fix two logic bugs found while refactoring + document the open ones
These are the bugs surfaced during the structural work that are simple
enough to fix in isolation. Larger ones (race conditions that need
session-token tracking, async-fetch ordering) are deferred and documented
in docs/ARCHITECTURE.md "Known logic bugs" — the repo has Issues
disabled, so the doc is the issue board.

== Fix: room history wiped on every tab close

src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx had a
useEffect that registered a `beforeunload` handler calling
`window.localStorage.removeItem('nitro.room.history')`. The whole point
of localStorage is to persist across sessions; wiping it on tab close is
either a leftover debug call or a misunderstanding of the API.

Removed the handler. History now persists across browser sessions, which
matches user expectations. If "session-only" was the intent, the right
primitive is `sessionStorage` (not localStorage + cleanup) — left as a
note in the doc.

== Fix: AvatarInfoPetTrainingPanelView null-pointer on session change

src/components/room/widgets/avatar-info/AvatarInfoPetTrainingPanelView.tsx
read `roomSession.userDataManager.getPetData(parser.petId)` without
guarding for `roomSession` being null. The PetTrainingPanelMessageEvent
can arrive during a room transition when `roomSession` is briefly null,
crashing the widget. Added `?.` chain on both `roomSession` and
`userDataManager`.

== Doc: known logic bugs section

Two open issues documented for follow-up:
- MainView.tsx CREATED/ENDED race — needs session-token tracking, fits
  cleanly into the future useNitroEventReducer companion to proposal #1.
- LayoutFurniImageView / LayoutAvatarImageView async fetch ordering —
  needs request-id refs, or solves itself once React Query (proposal #2)
  is enabled and the image fetch becomes a query keyed on props.

Plus a "recently fixed" subsection that records the four bugs already
addressed in this branch (doorbell close button, doorbell optimistic
remove, room history wipe, pet panel null-pointer) so the next reader
knows what changed and why.

== Verification

- yarn eslint on the two modified files: same error count before and
  after (5 pre-existing set-state-in-effect on RoomToolsWidgetView,
  none introduced).
- yarn tsc on the two modified files: clean.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:52 +00:00
simoleo89 48d62c5c6b Architecture refactor: docs + 5 pilot implementations + error boundary
This is the structural plan promised in the previous session, with concrete
pilots for all five proposals + the bonus error-boundary work.

== docs/ARCHITECTURE.md (new, ~370 lines)

Living document describing:
- where the project stands today (event-bus pattern friction with React 19,
  god-hooks, oversized files);
- the five proposed structural improvements with the why/how/status of each;
- what's already in place across this branch;
- recommended order for the next refactor PRs.

This is the deliverable the rest of this commit references.

== Proposal #3 + #4 pilots: src/features/doorbell/ (new)

Concrete feature-folder migration on the doorbell widget (chosen because
it's small enough to migrate end-to-end in one commit).

  src/features/doorbell/
    index.ts                    public API
    views/DoorbellWidgetView.tsx
    hooks/useDoorbellState.ts   reduces 3 events into a users array (data only)
    hooks/useDoorbellActions.ts answer(name, flag) (imperative actions only)

The split (data vs actions) is the pattern proposal #4 wants applied to
useCatalog/useChat/useWiredTools later. The original useDoorbellWidget had
both concerns + a buggy `useEffect(() => setIsVisible(!!users.length), [users])`
derive-state-in-effect. The new view computes visibility in render.

Compat shims kept so existing imports keep working:
- src/components/room/widgets/doorbell/DoorbellWidgetView.tsx -> 1-line re-export
- src/hooks/rooms/widgets/useDoorbellWidget.ts -> deprecated wrapper around
  the two new hooks, returning the same { users, answer } shape.

== Proposal #2 prototype: src/api/nitro-query/ (new)

Adapter outline for wrapping composer/parser request-response pairs in
TanStack Query. Not yet enabled because @tanstack/react-query is not in
package.json. The file documents the activation steps:

  yarn add @tanstack/react-query @tanstack/react-query-devtools
  + mount QueryClientProvider in src/index.tsx

awaitNitroResponse() throws with a helpful pointer to the doc section if
called before activation, so accidental adoption fails loudly.

== Proposal #5 skeleton: src/state/createNitroStore.ts (new)

Same pattern: skeleton + activation instructions. Not yet enabled because
zustand is not in package.json.

  yarn add zustand
  + replace the throw with `import { create } from 'zustand'; export const createNitroStore = create;`

The doc inside the file shows the recommended slice shape and points to
the suggested first migration target (the let isCreatingRoom singleton in
NavigatorRoomCreatorView).

== Bonus: WidgetErrorBoundary

src/common/error-boundary/WidgetErrorBoundary.tsx wraps react-error-boundary
with a sensible default (silent fallback, NitroLogger.error). Re-exported
from src/common/index.ts.

Applied as the umbrella around RoomWidgetsView's children — a widget
crash in a room (e.g. malformed pet data) now degrades gracefully
instead of unmounting the whole UI.

== Verification

- yarn eslint on all new + modified files: 0 errors / 0 warnings introduced.
  RoomWidgetsView still has its 1 pre-existing FC<{}> error (1 before, 1 after).
- yarn tsc on all new files: clean (only project-wide pre-existing
  TS2307 about @nitrots/nitro-renderer not installed locally remains).
- No regressions: existing imports of DoorbellWidgetView and
  useDoorbellWidget keep resolving via the compat shims.

== What's NOT in this commit (intentionally)

- Mass adoption of the new patterns elsewhere — left as follow-up PRs in
  the order documented in ARCHITECTURE.md "How to pick the next refactor PR".
- Installation of @tanstack/react-query / zustand — explicit team decision,
  not the LLM's to make.
- Test infrastructure (Vitest setup) — listed as the #1 missing piece in
  the doc, but a separate PR.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:52 +00:00
simoleo89 22a44d18b0 Add useNitroEventState / useMessageEventState hooks (proposal #1)
Introduce the building block for reducing the state-from-event
boilerplate that pervades the codebase:

  // Before
  const [foo, setFoo] = useState(initial);
  useNitroEvent(SOME_EVENT, e => setFoo(e.payload));

  // After
  const foo = useNitroEventState(SOME_EVENT, e => e.payload, initial);

Implementation notes:
- src/hooks/events/useNitroEventState.ts wraps useNitroEvent so the
  selector closure can use up-to-date surrounding values (captured in
  a ref refreshed in commit via useLayoutEffect) without forcing a
  re-subscription on every render. Listener is registered once and
  always reads the latest selector.
- src/hooks/events/useMessageEventState.ts is the mirror for
  useMessageEvent (server message channel — request/response composers
  and push parsers).
- Both pass the new react-hooks v7 rules cleanly (in particular the
  strict react-hooks/refs that forbids ref mutation during render).
- Re-exported from src/hooks/events/index.ts so callers reach them
  via the existing `from '../../hooks'` import path.

Pilot adoption (1 site) to demonstrate the pattern:
- src/components/catalog/views/targeted-offer/OfferView.tsx:
  the offer state was a clean derive-from-event case
  (setOffer(parser.data) on TargetedOfferEvent, no other writes).
  Replaced with a single useMessageEventState call using the optional
  chain `evt.getParser()?.data ?? null` as selector. Removes the
  useState pair and the explicit subscription block.

Honest scope note:
A broader sweep is intentionally NOT done. Most existing event
subscriptions in this codebase are multi-state updates, state
machines, conditional filters ("skip if not my id"), or have side
effects mixed in (notifications, redirects). Forcing those into
useNitroEventState would lose information and risk regressions in
behavior the lint won't catch. Adoption should happen organically
when contributors see a clean derive-from-event case, not as a
mechanical replace-all.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:52 +00:00
simoleo89 5d8717dedb Split WiredCreatorToolsView: extract types/constants/helpers into 3 sibling files
The single-file WiredCreatorToolsView.tsx was 4493 lines, which is one
of the main reasons the React Compiler reports
"Compilation Skipped: Existing memoization could not be preserved" on
this module. Split is conservative — only the pure leading sections
move out, the component itself is untouched (state, effects, JSX all
stay in place).

New files (sibling to the view):
- WiredCreatorTools.types.ts (~233 lines): every interface and type
  alias declared at the top of the original file.
- WiredCreatorTools.constants.ts (~225 lines): TABS, MONITOR_LOG_ORDER,
  poll constants, MONITOR_ERROR_INFO, INSPECTION_ELEMENTS,
  VARIABLES_ELEMENTS, EDITABLE_*, VARIABLE_DEFINITIONS,
  WIRED_FREEZE_EFFECT_IDS, TEAM_COLOR_NAMES, WEEKDAY/MONTH/DIRECTION
  names. The createVariableDefinition factory is kept as a local helper
  in this file (only used to build VARIABLE_DEFINITIONS).
- WiredCreatorTools.helpers.ts (~147 lines): createEmptyMonitorSnapshot,
  getHotelTimeFormatter (with its module-private cache map),
  getHotelDateTimeParts, formatMonitorLatestOccurrence,
  formatMonitorHistoryOccurrence, formatVariableTimestamp,
  formatMonitorSource, normalizeMonitorReason. All pure (or
  cache-stable), no closure on component state.

WiredCreatorToolsView.tsx changes:
- 4493 -> 3901 lines (-592, ~13% reduction).
- The four inspection-icon asset imports (furni/global/user/context)
  move to the constants file alongside the only consumers
  (INSPECTION_ELEMENTS / VARIABLES_ELEMENTS).
- AvatarInfoFurni was only referenced by the extracted
  InspectionFurniSelection interface and is removed from the main
  file's api import.
- New import block at the top pulls back the symbols actually used by
  the component body.

Verification:
- yarn eslint on the three new files: 0 errors / 0 warnings.
- yarn eslint on WiredCreatorToolsView.tsx: 26 errors before split,
  26 errors after split (identical pre-existing set; nothing new
  introduced).
- yarn tsc --noEmit on the four files: clean (only the project-wide
  pre-existing TS2307 about @nitrots/nitro-renderer not being
  installed locally remains, same as before).

This unblocks future per-tab splits (Monitor / Inspection / Variables
JSX panels are still inline in the view and represent the next ~1600
lines that could move out, but require introducing a shared state
context first since the current setState chain is intertwined).

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:52 +00:00
simoleo89 39eb2c6b84 Phase C (targeted): clear 4 set-state-in-effect violations on safe candidates
Fix only the cases that are unambiguous anti-patterns; leave the
event-driven setState patterns (useNitroEvent / useMessageEvent
subscriptions, async fetches with cleanup) alone since they're
legitimate in this architecture.

- src/components/catalog/views/catalog-header/CatalogHeaderView.tsx:
  displayImageUrl was pure-derived from imageUrl. Drop the useState +
  useEffect entirely; compute in render.
- src/components/navigator/views/NavigatorRoomCreatorView.tsx:
  the maxVisitors list (10..100 step 10) and roomModels/selectedModel
  came from static config; convert to module-level MAX_VISITORS_LIST
  constant + useState lazy initializers. Removes 2 init effects.
  setCategory(categories[0].id) is left as-is because categories
  arrives async from a hook.
- src/components/login/LoginView.tsx:
  Replace useEffect(() => setLocalError(null), [step]) with the
  React-recommended "track previous prop" render-time reset:
  if(prevStep !== step) { setPrevStep(step); setLocalError(null); }
  Same observable behavior, no extra render.
- src/components/room/widgets/choosers/ChooserWidgetView.tsx:
  Wrap the selectItem callback prop call in useEffectEvent so a
  parent re-render that changes selectItem identity doesn't
  re-fire the visualizer side-effects.

Net: 4 fewer set-state-in-effect violations; behavior preserved.
The remaining ~328 violations across the codebase are predominantly
legitimate event-bus / async-fetch patterns and need per-case
review with running app, not a sweep.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:52 +00:00
simoleo89 d382635597 Phase A: clear all react-hooks/exhaustive-deps warnings via useEffectEvent or hoisting
Eliminate the four remaining missing-dependency warnings reported by
react-hooks v7. Each one was a real stale-closure or re-trigger hazard;
the fix matches the intent rather than just silencing the linter.

- src/App.tsx (line 448): wrap showSessionExpired with useEffectEvent
  (onSessionExpired) so the prepare effect doesn't re-run on every
  showSessionExpired identity change but still calls the latest
  callback. Replace the two in-effect call sites.
- src/components/furni-editor/views/FurniEditorSearchView.tsx: wrap
  the on-mount onSearch('', '', 1) call with useEffectEvent so the
  callback prop isn't a missing dependency.
- src/components/notification-center/views/bubble-layouts/
  NotificationBadgeReceivedBubbleView.tsx: wrap the
  "fetch badges only if empty on mount" check with useEffectEvent
  so badgeCodes.length isn't required as a dep (and won't re-fetch
  every count change).
- src/components/navigator/views/room-settings/
  NavigatorRoomSettingsRightsTabView.tsx: switch deps from
  roomData?.roomId to roomData (the body uses roomData.roomId after
  an early return; the linter wanted the whole object).
- src/api/ui-settings/UiSettingsContext.tsx: hoist ALL_CSS_VARS
  outside the component (it's a static constant).

After this, yarn eslint reports zero exhaustive-deps warnings across
the whole src/.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:51 +00:00
simoleo89 f18c917fc4 Add TypeScript 7 (tsgo) as fast type-checker alongside TS 6
TypeScript 7 is the Go-native rewrite of tsc, ~10x faster but only
distributed as @typescript/native-preview daily builds at the time
of writing (npm typescript@latest is still 6.0.3). Add it as a
non-disruptive type-check tool: yarn typecheck → tsgo --noEmit.

Vite still uses esbuild for transpilation, ESLint still uses TS 6
through @typescript-eslint v8, IDEs continue using their bundled TS.
This commit only adds a type-check tool — nothing replaces.

Required tsconfig.json adjustments for TS 7 compatibility (still
valid for TS 6):

- Drop baseUrl: "./src" (removed in TS 7). The codebase has no
  bare/non-relative imports that depended on it; all imports are
  relative or aliased.
- Drop downlevelIteration: true (removed in TS 7; target es2022
  doesn't need it).
- moduleResolution: "node" → "bundler" (TS 7 dropped node10; bundler
  is the right mode for Vite anyway).
- paths "@layout/*" entries now use leading "./" (TS 7 disallows
  non-relative path mappings). Add "@/*" → "./src/*" to match the
  Vite alias used in some components.

Other TS 7 adjustments:

- src/react-app-env.d.ts: add module declarations for *.css/.scss/.sass
  side-effect imports (TS 7 with bundler resolution requires them) +
  Window.NitroConfig / Window.NitroSecureApiUrl globals which were
  used in App.tsx without a declaration.
- src/common/Popover.tsx: explicit `import { JSX } from 'react'`
  because TS 7 dropped the implicit global JSX namespace.

Verification:
- yarn eslint still passes (TS 6 / @typescript-eslint v8 happy with
  the migrated config).
- yarn typecheck (tsgo) runs and reports only cascading errors
  rooted in the missing @nitrots/nitro-renderer sibling repo
  (environmental, not introduced here).

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:51 +00:00
simoleo89 6c9f414028 Apply useEffectEvent (React 19.2) to TurnstileWidget callbacks
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
2026-05-11 16:31:51 +00:00
simoleo89 5697d169ee Fix rules-of-hooks violation in InfiniteGrid
InfiniteGridRoot called useVirtualizer + 2 useEffect after an early
return for the squareItems branch, which violates the rules of hooks
(react-hooks v7 now flags this as an error and react-compiler skips
the component entirely).

Split the component into three:

- useColumnMeasure: shared custom hook that owns parentRef +
  ResizeObserver-based column measurement (used by both branches).
- InfiniteGridSquare: the non-virtualized grid for squareItems mode.
  Doesn't call useVirtualizer.
- InfiniteGridVirtualized: the virtualized branch with TanStack
  Virtual + scroll/padding effects.

InfiniteGridRoot becomes a thin selector that routes by props.squareItems.
All hooks in each sub-component are now unconditional.

The remaining lint findings on this file (set-state-in-effect inside
InfiniteGridItem, react-hooks/incompatible-library on useVirtualizer)
are pre-existing/informational and out of scope.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:51 +00:00
simoleo89 13dc483938 Bump ecosystem dependencies (minor/patch)
- @radix-ui/react-popover: ^1.1.6 → ^1.1.15
- @radix-ui/react-slider:  ^1.2.4 → ^1.3.6
- react-icons:             ^5.5.0 → ^5.6.0
- dompurify:               ^3.4.1 → ^3.4.2
- @tanstack/react-virtual: pin → ^3.13.24 (loosen to caret; no version change)

framer-motion (12.38.0), @tanstack/react-virtual (3.13.24), emoji-mart
(5.6.0), and @emoji-mart/* are already at latest. react-player was
intentionally not bumped (3.x is a major release).

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:51 +00:00
simoleo89 25d51aff3f Enable <StrictMode> + make App.tsx renderer init idempotent
App.tsx's prepare() useEffect ran four .init() calls
(SessionDataManager, RoomSessionManager, RoomEngine, Communication)
without any guard, plus an immediate heartbeat ping and a legacy
authentication track. Under StrictMode dev double-invoke, those
fire twice — risking duplicate session/communication state.

- Gate the four .init() chain behind gameInitPromiseRef: both the
  first and the simulated second invocation await the same promise.
- Gate the legacy track + immediate heartbeat behind bootstrapDoneRef.
- Heartbeat and remember-rotate intervals were already idempotent
  (clearInterval before setInterval); ticker registration was already
  guarded by tickersStartedRef; renderer/warmup were already gated by
  rendererPromiseRef/warmupPromiseRef. No change needed there.

Wrap <App /> in <StrictMode> in src/index.tsx now that the renderer
init path is double-invoke safe.

https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
2026-05-11 16:31:50 +00:00
simoleo89 535fa71020 ESLint --fix: auto-fix brace-style, indent, semi, no-trailing-spaces
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
2026-05-11 16:31:50 +00:00
simoleo89 1b1e0c18bf React 19 Phase 3: login/forgot/register forms → useActionState + useFormStatus
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
2026-05-11 16:31:50 +00:00
simoleo89 a1bee1d825 React 19 modernization: forwardRef removal, Compiler, ErrorBoundary, Suspense, native <script>
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
2026-05-11 16:31:50 +00:00
DuckieTM 2137d23ac0 Merge pull request #125 from duckietm/Dev
🆙 Fixed the commands
2026-05-08 16:45:02 +02:00
duckietm d88defb4a5 🆙 Fixed the commands 2026-05-08 16:44:45 +02:00
DuckieTM 98e250e49a Merge pull request #124 from duckietm/Dev
Dev
2026-05-08 11:58:48 +02:00
duckietm 6124610736 🆙 Small fix Avatar loading & moved news to path wich you can enter
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",
2026-05-08 11:58:32 +02:00
DuckieTM fa3bb7b9ac Merge pull request #123 from Lorenzune/merge-duckie-main-2026-05-06
Refine mobile avatar widgets and login flow
2026-05-08 07:45:28 +02:00
DuckieTM 71725b7f67 Merge branch 'Dev' into merge-duckie-main-2026-05-06 2026-05-08 07:45:17 +02:00
Lorenzune 57b83c1097 Refine mobile avatar widgets and login flow 2026-05-07 21:19:15 +02:00
DuckieTM 83ee05957d 🆙 Fix font colors in chat 2026-05-07 20:49:50 +02:00
DuckieTM 247ada2fb5 Merge pull request #121 from duckietm/Dev
🆙 Fix when using : --base=/client/ in package.json
2026-05-07 15:17:12 +02:00
duckietm 3d88ec8cfc 🆙 Fix when using : --base=/client/ in package.json 2026-05-07 15:16:54 +02:00
DuckieTM 72fea20e0b Merge pull request #120 from duckietm/Dev
🆙 Small update
2026-05-07 10:24:16 +02:00
duckietm 5e26514143 🆙 Small update 2026-05-07 10:21:48 +02:00
DuckieTM 4da1af605d Merge pull request #119 from duckietm/Dev
Dev
2026-05-06 12:50:29 +02:00
duckietm dbf5ae875c 🆙 Small update in building 2026-05-06 12:50:00 +02:00
duckietm 7396413f11 🆙 Fixed Multiple SCSS-isms ended up inside plain .css 2026-05-06 10:48:09 +02:00
duckietm f6b7a3c9d7 🆙 Update Render example 2026-05-06 10:38:55 +02:00
duckietm aa20b0acbe 🆙 Small fix Background to load from json 2026-05-06 08:57:44 +02:00
duckietm a243640741 🆙 News windows in UI login update 2026-05-06 08:48:17 +02:00