The Context strip at the top of the launcher showed which room the
mod is currently observing — green pill + door icon when in a room,
zinc strip when not. In practice it's noise: the Room Tool / Chatlog
Tool buttons right under it already gate on the same in-room state
(disabled when not in a room) and carry their own tooltip explaining
that. The strip duplicated the signal without adding actionable info.
Remove the section, the now-unused FaDoorOpen / FaDoorClosed imports,
and the matching `modtools.window.section.context` /
`modtools.window.context.room` locale keys (from both the runtime
UITexts.json and the versioned UITexts.example template).
User reported empty fields (Email, Last Purchase, Lock Expires, Banned
Accs, Abusive CFHs) showing what looks like a music-note glyph next to
the label. They aren't censored — they're genuinely empty (a rank-7
Administrator account has none of that data populated). The em-dash
"—" (U+2014) used as the placeholder doesn't have a glyph in the
Habbo pixel font (Volter / Volter-Goldfish), so the engine falls
through to a placeholder glyph that on some font stacks looks like a
music note.
Two-part fix in ModToolsUserView (Field), ModToolsIssueInfoView
(Field) and ModToolsRoomView (owner fallback):
1. Replace the U+2014 em-dash with a plain ASCII `-`. Hyphen-minus is
safely in Volter, so the placeholder renders correctly across the
whole client.
2. The `value || placeholder` guard is now `(value || value === 0)`.
Stat fields whose value is the literal number 0 — a clean account
with cfhCount=0, banCount=0, cautionCount=0 — were rendering the
placeholder because 0 is falsy. Treat 0 as a real value.
Also dropped the `italic` class on the placeholder span — the
hyphen does the job on its own and italic on a single-character
glyph in a pixel font was making it look like a tilted line.
The launcher panel was a flat stack of four buttons (Room Tool, Chatlog
Tool, selected-user + presence dot inline, Report Tool) with no visual
hierarchy. The selected-user row was particularly cramped — name, the
2px dot and the 4×4 close-X all crammed into a single button row, easy
to misclick.
Reorganize into four logical groups, each with a small uppercase
section label:
Context — gradient strip (emerald when in a room, zinc when not)
showing "Room #<id>" or "Enter a room first" with a
matching door icon. Source of truth for "what is the
mod observing right now"; both Room Tool and Chatlog
Tool feed from the same currentRoomId.
Room — Room Tool + Chatlog Tool stacked. Both still gate on
isInRoom; the disabled state now reads from a single
flag instead of repeating `currentRoomId <= 0`.
User — When a user is selected: a card with the presence dot
(emerald = still in room, zinc = left), the username at
a real legible size, a bigger close button, plus a
dedicated "Open Info" button to toggle ModToolsUserView.
Splitting the click target from the close action removes
the misclick footgun.
When no user is selected: a dashed-border empty state
with a FaUserSlash icon and the "Select a user" hint —
reads as a clear "no selection" instead of an active
button you can't press.
Reports — Report Tool with the open-ticket badge. Badge gets a 2px
rose halo box-shadow so a new ticket pulses into view
instead of competing with the button background.
Locale keys added under modtools.window.section.* and
modtools.window.context.room / modtools.window.user.open_info, in both
the runtime UITexts.json and the versioned UITexts.example template.
The "Open Info" button label is a fix in flight — the old layout
overloaded the username row to also open user info, with no separate
label. The new explicit button gets its own key so the action is
unambiguous (the previous version mislabelled the button as "Mod
Action", which is actually a different sub-panel).
typecheck + vitest 214/214 + JSON validation all clean.
ModToolsChatlogView and CfhChatlogView were on the useNitroQuery
pattern. Symptom: the card opens, the spinner spins, but the data
never arrives — even when the server is correctly answering with
ModToolRoomChatlogComposer (header 3434) and GetCfhChatlogComposer
(607). Both header IDs match the renderer's Incoming map, both server
handlers gate only on ACC_SUPPORTTOOL and reply unconditionally when
the room/issue lookup succeeds. So the request DOES go out and the
response DOES come back — but useNitroQuery's listener (registered
via `new (ParserCtor)(callback)` + `registerMessageEvent`) isn't
delivering the event to the React side here.
ModToolsUserChatlogView already uses the plain `useMessageEvent` +
`useEffect(sendComposer)` pattern and works on this same setup, so
align the two broken views with it. Keep the loading-spinner empty
state introduced yesterday so the user still gets visible feedback
while the response is in flight.
This sidesteps useNitroQuery for these two cases rather than fixing
it in place — the underlying createNitroQuery + listener registration
plumbing still works for OfferView, useUserGroups, useClubOffers,
useSellablePetPalette, useMarketplaceConfiguration, useClubGifts,
CatalogLayoutRoomAdsView, so the regression is specific to these two
parsers and worth investigating separately. Filed as a follow-up.
ModToolsChatlogView returned null whenever roomChatlog was undefined
— including the entire window between click and server response (up
to a 15-second NitroQuery timeout). Result: clicking the Chatlog
button in the launcher or in Room Info appeared to do nothing at all
on any session where the server reply was slow or the accept-filter
correlation didn't match.
The other two chatlog wrappers (ModToolsUserChatlogView,
CfhChatlogView) already render a spinner while data is loading after
yesterday's redesign — this view was the one I missed.
Apply the same fix: always render the NitroCardView, and show the
FaSpinner loading state inside until useNitroQuery resolves.
The ModTools template refresh introduced ~80 hardcoded English strings
(labels, placeholders, tooltips, empty-state copy, button text). Move
every one of them onto the modtools.* namespace and read via
LocalizeText so the panels translate alongside the rest of the client.
UITexts.example (versioned template) extended with the full set:
modtools.window.* Launcher box (toolbar item, tools,
selected-user state, ticket count)
modtools.userinfo.* User info card — already had the
legacy modtools.userinfo.{userName,
cfhCount, …} keys from before; added
refresh tooltip, presence pill labels
(in_room / online / offline with
matching .title tooltips), section
headings, action button labels, stat
card labels
modtools.roominfo.* Room info card — title, refresh, loading,
owner pill (here/away + tooltips), stat
labels, action buttons, moderate panel
heading + checkboxes + textarea
placeholder + caution/alert CTAs
modtools.user.message.* Send-message dialog (recipient label,
body label, placeholder, char counter,
empty state, send button)
modtools.user.modaction.* Mod Action form — header, sanctioning
label, 3-step section titles, select
placeholders, message label + optional
note, message placeholder, preview
heading, default/apply buttons, every
sendAlert error message
modtools.user.visits.* Room visits — title, header strip
heading, entry count (singular/plural),
empty state, column headers, visit
button + tooltip
modtools.user.chatlog.* User chatlog — title (with username
variant), loading state
modtools.room.chatlog.* Room chatlog title
modtools.chatlog.* Shared ChatlogView — column headers,
empty state, room-separator Visit/Tools
buttons
modtools.tickets.* Tickets window — title, tab labels
(open/mine/picked), column headers,
empty states, action buttons (pick/
handle/release), issue resolution
window (title, label, details heading,
field labels, chatlog toggle, resolve-as
heading, resolution buttons, release
back to queue), CFH chatlog title
The same 130 entries land in Nitro-Files/.../UITexts.json (runtime).
Both files validate as JSON. The runtime additions take effect on
next client reload; the template additions ship the strings to any
fresh deploy.
Notes:
- The MOD_ACTION_DEFINITIONS sanction names ("Alert", "Mute 1h",
"Ban 18h" …) stay hardcoded for now since they're keyed off
server-side action IDs that don't have an existing locale key
convention. Worth a follow-up if needed.
- help.cfh.topic.* keys (CFH topic display names) are already in
ExternalTexts.json and were already read via LocalizeText, so
they didn't need changes.
typecheck + vitest 214/214 + lint:hooks all clean.
Applies the visual language introduced in ModToolsUserView yesterday
to every other ModTools window. The design tokens used consistently:
emerald — present in current room / positive state
sky — online / informational / current selection
zinc — neutral / disabled
amber — warn-level (CFH, alerts, cautions)
rose — danger (bans, releases, abusive)
Files redesigned:
ModToolsRoomView
Identity header with FaDoorOpen, room name + ID, owner-present pill
(emerald/zinc), manual refresh button. Stat strip: user count (sky)
+ clickable owner name (zinc) opening user info. Quick actions
(Visit / Chatlog) in a 2-col grid. Moderate panel collapsed into an
amber-tinted card with the 3 toggles + textarea + two CTAs (Send
Caution=danger, Send Alert=warning). CTAs disabled until a message
is typed AND the room info has loaded.
ModToolsUserModActionView
Numbered 3-step form (CFH topic → sanction → optional message).
Live preview row showing the chosen topic + sanction as tone-coded
pills (amber/sky/rose/orange/fuchsia/zinc by action type). Primary
CTA = Default Sanction, success CTA = Apply Sanction, both
disabled until the required selections are made.
ModToolsUserSendMessageView
Recipient header with FaEnvelope and the username, autofocused
textarea, char counter, single full-width Send button gated on
non-empty message.
ModToolsUserRoomVisitsView
Header strip with entry count badge, three-column grid (time / room
name / visit button), monospace timestamps, hover row highlight,
empty state with FaDoorOpen icon.
ModToolsUserChatlogView / ModToolsChatlogView / CfhChatlogView
Loading state with spinner instead of returning null. Cards grow to
min-w-[460px] max-w-[520px] max-h-[500px] for usable chatlog area.
ChatlogView
Replace Bootstrap-ish striped table with a CSS grid (60px / 120px /
1fr). Room-info separator rendered as a sky card with Visit/Tools
pill buttons. Per-row hover + even-row tint; highlighted rows
(hasHighlighting) get an amber wash. Username is a button opening
user info via existing link event. Empty state with FaCommentDots.
ModToolsTicketsView
Tabs get icons (FaListUl / FaUserCheck / FaCheckSquare) and inline
count badges (amber/sky/zinc) so the moderator sees the queue size
at a glance. ticket bucket filtering memoized off the tickets array.
ModToolsOpenIssuesTabView / MyIssuesTabView / PickedIssuesTabView
Same CSS grid table style. Category renders as a tone-coded pill
(Open=amber, Mine=sky, All picked=zinc). Action buttons get icons
(FaHandPointer Pick, FaTools Handle, FaSignOutAlt Release). Empty
state with FaInbox.
ModToolsIssueInfoView
Card header with category + topic pills. Details rendered as a dl
grid instead of a striped table. Caller / Reported names as inline
link buttons with external-link icon. Chatlog toggle is full-width
secondary. Resolution buttons in a 3-col grid with intent colours
(success=Resolved, dark=Useless, danger=Abusive) + a separate
Release-to-queue button on its own row so it isn't confused with
the resolutions.
No behaviour changes — all composers, message events, parent state
hookups, and sanction validation paths are unchanged. This is purely
a presentation pass. typecheck + vitest 214/214 + lint:hooks all
clean.
Replace the flat striped table with a structured layout that surfaces
the moderation signal at a glance:
Identity header
Username + ID + classification, presence pill (In room / Online /
Offline) with colour coding (emerald / sky / zinc) and a matching
dot, plus a manual refresh button. The pill source-of-truth is
useRoomUserListSnapshot for the "in room" case (reactive) falling
back to userInfo.online — tooltip discloses which path produced
the value.
Stat strip
Four counter cards in a single row — CFH, Cautions, Bans, Trade
locks — tinted warn (amber) or danger (rose) when value > 0, neutral
(zinc) when zero. Big tabular-nums numbers so the moderator sees a
problem account immediately without parsing rows.
Sectioned body
Account / Activity / Sanctions / Trading as labelled dl groups
(grid-cols-[auto_1fr]) replacing the 14-row striped table. Missing
values render as a dim em-dash instead of an empty cell.
Action bar
2×2 button grid with react-icons/fa glyphs (FaCommentDots,
FaEnvelope, FaDoorOpen, FaGavel). Mod Action keeps variant="danger"
so the destructive action stands out from the three info actions
(variant="secondary").
No behaviour changes — the same composer / event listeners /
sub-views are wired up; this is a presentation rewrite. Card grows
to min-w-[420px] max-w-[480px] to fit the new layout without
horizontal scroll on mod laptops.
ModToolsUserView used a one-shot ModeratorUserInfoData snapshot taken at
panel-open time. Two consequences:
- The online/offline icon (rendered next to userName) was frozen on the
value at open. If the target user joined/left while the panel stayed
open, the icon kept lying.
- After the moderator applied a sanction via ModToolsUserModActionView
the user info window stayed open with stale cfhCount / banCount /
cautionCount / lastSanctionTime; you had to close and reopen to see
the bump.
Fix shape mirrors the ModToolsView selected-user dot from yesterday:
- Read useRoomUserListSnapshot in the component (outside any useBetween
scope — useSyncExternalStore constraint). If the target user is in
the current room they're online; fall back to userInfo.online
otherwise. Tooltip surfaces which path produced the value.
- Subscribe to ModeratorActionResultMessageEvent (parser carries
userId + success). On a successful action targeting THIS userId,
re-send GetModeratorUserInfoMessageComposer so the table re-fetches.
ModToolsView
- Subscribe to useRoomUserListSnapshot so the selected user's
"still in room" state is reactive — green dot when the user is
in the current room, gray dot when they've left. Previously the
selection was a static capture at click time.
- Add an inline X to clear the selected-user slot without having
to click a different avatar.
- Report Tool button shows a count badge for OPEN tickets
(IssueMessageData.STATE_OPEN) so a new ticket arriving while
the panel is open is visible immediately. Caps display at 99+.
- Tooltip on the room-bound buttons explains why they're disabled
("Enter a room first") instead of showing a silent disabled state.
- Buttons grow their labels with `flex-grow text-start` so the
trailing dot / badge / clear-X sits flush right.
useModTools
- Fix splice(index) → splice(index, 1) in close{Room,RoomChatlog,
UserInfo,UserChatlog} — the omitted second argument was
silently deleting EVERY subsequent open panel, not just the one
being closed. Visible whenever a moderator had two or more panels
of the same kind open.
- Fix toggleUserChatlog reading from openRoomChatlogs instead of
openUserChatlogs — copy-paste typo made the toggle inconsistent
with the underlying state.
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.
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
Replace ~70 hardcoded English strings across 15 mod-tools files
with LocalizeText() calls using moderation.* keys matching the
existing ExternalTexts convention. Includes mod-tools-external-texts.json
with all required keys for ExternalTexts.json.
- Fix icon alignment using flexbox instead of absolute positioning
- Add active state indicators on buttons when sub-panels are open
- Add min-width constraints to prevent cramped layouts
- Improve user button with placeholder text and truncated username
- Improve room info panel with better spacing, clickable owner, colored owner status
- Improve chatlog with scrollable container, alternating row colors, compact headers
- Clean up room info header and room name display