Replace the rank-level family (useHasRankLevel + STAFF_LEVELS
constants + useIsRank) with a permission-driven family that reads
straight from the deployment's `permission_definitions` table — no
more hardcoded SecurityLevel/rank-id thresholds on the client. A new
rank in permission_ranks or a re-shuffling of permission_definitions
rank columns now propagates through the UI automatically.
Renderer-side wire shipped in companion commit
feat/react19-event-bus@159c5eb (UserPermissionsMapParser + Event,
SessionDataManager.getPermissionsSnapshot + USER_PERMISSIONS_UPDATED).
New public API in `useSessionSnapshots.ts`:
- useUserPermissions(): ReadonlyMap<string, number> — full map
- useHasPermission(key): boolean — > 0 ⇒ true
- usePermissionValue(key): number — raw 1/2 or 0
- useIsAmbassador() now aliases useHasPermission('acc_ambassador')
- useUserRank() kept for PRESENTATIONAL use only (badge, prefix,
prefix color) — documented as such in JSDoc; gating must use
useHasPermission.
Dropped:
- src/api/nitro/session/RankLevels.ts (STAFF_LEVELS constants)
- useHasRankLevel / useIsRank exports (rank-based gating)
11 consumer migrations, each mapped to the right
`permission_definitions.permission_key`:
- ToolbarView (mod-only chat-input button) → acc_supporttool
- ChooserWidgetView (room-object id column) → acc_supporttool
- CatalogClassicView (admin toggles) → acc_catalogfurni
- CatalogModernView (admin toggles) → acc_catalogfurni
- FurniEditorView (panel access) → acc_catalogfurni
- CalendarView (force-open day) → acc_calendar_force
- InfoStandWidgetFurniView (mod buildtools btn) → acc_anyroomowner
- AvatarInfoWidgetPetView (canPickUp) → acc_anyroomowner
- FurnitureMannequinView (controller mode) → acc_anyroomowner
- YouTubePlayerView (isMyRoom) → acc_anyroomowner
- NavigatorRoomInfoView 'settings' → acc_anyroomowner
- NavigatorRoomInfoView 'staff_pick' → acc_staff_pick
Test refresh:
- useUserRank still tested for the presentational shape.
- useHasPermission: true for non-zero, false for absent/zero.
- usePermissionValue: raw 1 / 2 / 0 (default).
- useUserPermissions: full map exposure.
- Runtime promote test: mutate the permissions map + dispatch
USER_PERMISSIONS_UPDATED, assert useHasPermission flips false→true.
Locks in the new reactive contract.
Mock unchanged (the test sets getPermissionsSnapshot via vi.mocked).
Verification: yarn typecheck clean, yarn lint:hooks clean, yarn test
214/214 (213 prior + 1 net new for useUserPermissions). Backward
compatible: older Arcturus deployments don't ship the map → empty
snapshot → every gate is false → mod UI hidden (safe default).
Drop the SecurityLevel-named family (useIsModerator / useIsAdmin /
useIsCommunity / useIsPlayerSupport / useHasSecurityLevel /
useUserSecurityLevel) in favour of a rank-based family tied to the
operator's actual `permission_ranks` rows in the Arcturus DB:
- `useUserRank()` returns `{ id, name, level, badge, prefix,
prefixColor }` derived from the snapshot. Powered by the renderer's
extended IUserDataSnapshot (companion commit 87e67d5 on
feat/react19-event-bus).
- `useHasRankLevel(min)` replaces useHasSecurityLevel; consumers
pass a `permission_ranks.level` threshold from the deployment.
- `useIsRank(name)` matches `permission_ranks.rank_name` exactly.
To avoid bare integers in widget bodies, added a deployment-scoped
constants file at `src/api/nitro/session/RankLevels.ts`:
export const STAFF_LEVELS = {
MEMBER: 1, SUPPORT: 4, MOD: 5, SUPER_MOD: 6, ADMIN: 7
};
A deployment that re-numbers `permission_ranks` only edits this file.
Migrated all 11 consumer reads (same set as the earlier session's
useIsModerator migration plus the audit catch): ToolbarView,
CatalogClassicView, CatalogModernView, ChooserWidgetView,
CalendarView, YouTubePlayerView, FurniEditorView,
InfoStandWidgetFurniView, AvatarInfoWidgetPetView,
FurnitureMannequinView, NavigatorRoomInfoView. The
NavigatorRoomInfoView `staff_pick` permission was previously
`securityLevel >= COMMUNITY (7)` via the renderer-enum wrapper —
ported to `useHasRankLevel(STAFF_LEVELS.ADMIN)` because in the
default seed level 7 is Administrator, which is the actual rank that
gets the `acc_anyroomowner`-style permissions for staff-picking.
Tests refreshed under `useSessionSnapshots.test.tsx`:
- useUserRank surfaces the full metadata block;
- useHasRankLevel does `>=` against the threshold;
- useIsRank exact-matches against rank_name;
- a runtime promote (snapshot mutation + SESSION_DATA_UPDATED
dispatch) flips the result, locking in the reactive contract.
Mock extended only minimally — kept the SecurityLevel enum class for
any consumer outside the dropped family that still imports it.
Verification: yarn typecheck clean, yarn lint:hooks clean, yarn test
213/213. The Arcturus-side composer change (UserPermissionsComposer
appending the 5 extra fields) is staged but UNCOMMITTED on Arcturus
main (which has unrelated WIP); the wire is backward-compatible so
the React client works against both pre- and post-extension
emulators.
Build on the useIsModerator landing (532cb28c) along three axes:
1. Family. Extract `useHasSecurityLevel(min)` as the primitive,
backed by a fresh `useUserSecurityLevel()` raw-level reader. The
six SecurityLevel constants (1..9) deserve named wrappers so the
"show this only to X-and-up" pattern doesn't get re-derived ad-hoc
each time: shipped `useIsModerator` / `useIsPlayerSupport` /
`useIsCommunity` / `useIsAdmin` as one-line shims. Also added
`useIsAmbassador()` as a sibling — not derived from security level,
reads the boolean field on the snapshot directly.
2. Audit. The 532cb28c migration covered 6 React-render reads but
missed 5 more discovered by a follow-up grep:
- FurniEditorView (top-level `const isMod`)
- InfoStandWidgetFurniView (inline JSX, mod-only build-tools button)
- NavigatorRoomInfoView (3 reads in hasPermission(): isModerator
and securityLevel >= COMMUNITY for the staff-pick gate. The
userId read stays imperative — userId doesn't flip at runtime in
practice, no reactivity gain.)
- AvatarInfoWidgetPetView (inside useMemo with [roomSession] deps;
migrated and isModerator added to the deps so a runtime
promote/demote re-derives canPickUp without remount)
- FurnitureMannequinView (inside useEffect; same treatment — added
isModerator to the deps so the mode re-resolves on flip)
The remaining ~17 reads (CanManipulateFurniture,
AvatarInfoUtilities.populate*, useChatInputActions,
useFurnitureDimmerWidget / useFurniturePlaylistEditorWidget /
useFurnitureStickieWidget canModify checks, useCatalog admin
filter, useNavigator door-mode guard) are click-time / event-time
imperative — they read at the moment a user action fires, so a
reactive value would be cached at hook execution and stale by the
time the action runs. Leaving them on the synchronous manager read
is correct.
3. Test. Added four cases pinning the contract:
- useUserSecurityLevel returns the raw level.
- useHasSecurityLevel does `>=` against the threshold.
- Named wrappers map to the right constants (MODERATOR=5,
COMMUNITY=7, ADMINISTRATOR=8).
- **Reactive flip** — mutate the snapshot, dispatch the
SESSION_DATA_UPDATED event on the mock dispatcher, assert the
hook re-derives. Locks in the whole point of the snapshot
pattern (a static read would pass cases 1-3 but fail case 4).
Mock changes:
- Added SecurityLevel class (mirrors the renderer enum 0..9) so the
family wrappers resolve to actual numbers in jsdom — without it
`useIsModerator()` would call `useHasSecurityLevel(undefined)` and
the test would silently pass false-positives.
Verification: yarn typecheck clean, yarn lint:hooks clean, yarn test
213/213 (209 baseline + 4 new family/reactivity cases).
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 the Furni Editor from REST API to WebSocket-based communication
using custom packet handlers (10040-10046). The editor now communicates
directly with the emulator for all CRUD operations on furniture items.
Key changes:
- Replace REST API calls with WebSocket composers/parsers for search,
edit, create, and delete operations
- Read furnituredata.json path from renderer-config.json for asset
management
- Improve search UI with larger fonts, better contrast, and click-to-copy
ID functionality with toast notification
- Compact edit view layout with collapsible sections and visual dividers
- Remove unused Create tab (creation handled via edit workflow)
- Add isModerator guard for admin-only access
- Support search by ID, name, or sprite ID with type filtering
- FurniEditor component with Search/Edit tabs (NitroCard UI)
- useFurniEditor hook connecting to Next.js API routes via Vite proxy
- Edit Furni button in room infostand (godMode) with sprite ID lookup
- Toolbar: 3-column flex layout (icons | chat | friends)
- Heroicons SVG for ID/Sprite display in infostand and edit view
- Vite config: proxy /api to Next.js, aliases for renderer3 packages