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.
Adds a reactive `useIsModerator()` derived from
`useUserDataSnapshot().securityLevel >= SecurityLevel.MODERATOR`
(mirrors the renderer-side getter at SessionDataManager.ts:684), and
migrates the six React component-body reads of
`GetSessionDataManager().isModerator`:
- ToolbarView (mod-only chat-input button)
- CatalogClassicView, CatalogModernView (admin toggles in catalog
header)
- ChooserWidgetView (room-object id column visibility)
- YouTubePlayerView (room-control affordance — hook moved above the
`if (!isOpen) return null` early return so the hook order stays
stable when the player opens/closes)
- CalendarView (mod-only "open all" affordance)
UX impact: any future promote/demote that flips
SESSION_DATA_UPDATED now re-renders the mod-only UI live, instead of
requiring an F5. Imperative call sites
(AvatarInfoUtilities.populate*, CanManipulateFurniture,
RoomChatHandler) still read the manager directly — they run at click
time, not in a React render, so reactivity has no upside there.
Five of the six call sites are top-level component-body reads (no
early-return interaction). YouTubePlayerView has an
`if (!isOpen) return null` below the hook list, so the hook had to
move ABOVE it; same shape as the recent CatalogPurchaseWidgetView and
CatalogItemGridWidgetView fixes.
Verification: yarn typecheck clean, yarn lint:hooks clean, yarn test
209/209.