Adds a compact collapsible radio widget (top-left) that plays internet
radio streams with the HTML5 Audio API — no server/renderer changes.
- station list loaded from a JSON5 config file (loadGamedata: JSON + JSON5),
shipped as radio-stations.json5.example so each hotel fills in its own
- shows the selected station + a dropdown (3 visible, scrolls if more) to
switch; volume slider; animated equalizer + LIVE indicator
- first station autostarts quietly (5%) on load, with a resume-on-first-
gesture fallback for browser autoplay policy
When the server (soundboard_sounds table) returns no pads, the client now
loads them from a JSON5 config file (loadGamedata accepts plain JSON and
JSON5). Useful when the DB / CMS isn't set up yet.
File-defined pads play locally for the clicker; DB-backed pads still go
through the server broadcast so everyone in the room hears them. Ships a
radio-style soundboard-sounds.json5.example template.
Client side of the soundboard. Room owners enable it in Room Settings >
Misc (next to the YouTube TV toggle). When enabled, a soundboard icon
appears in the toolbar for everyone in the room; pressing a pad broadcasts
the sound so all occupants hear it. Incoming SoundboardPlay is played via
the HTML5 Audio API.
Also: fix FloorplanCanvasSVG to use ReactElement instead of the removed
global JSX namespace (React 19), and pair the client Dev branch with the
renderer fork that carries the custom features in CI.
How sounds are managed (works with any CMS):
Sounds are rows in the `soundboard_sounds` table:
id, name, url, enabled, sort_order
The emulator loads every row with enabled=1 (ordered by sort_order, id)
and sends the list to clients on room enter; the client plays `url`
directly, so any publicly reachable audio URL works (mp3/ogg/wav).
To add a sound from an admin/housekeeping panel of any CMS:
1. Upload the audio file to wherever the CMS stores public assets
(same approach as custom badge images).
2. INSERT a row into `soundboard_sounds` with the display name and the
public URL of the uploaded file, enabled = 1.
3. Reload the emulator soundboard (or restart) to pick it up.
Relative urls resolve against the `soundboard.url.prefix` config key
(falls back to `asset.url`); absolute urls are used as-is.
Two things in one commit because they sit on top of each other:
1. **Reset password reveal card.** The emulator's
`HousekeepingResetUserPasswordEvent` already returns the freshly
generated 12-char plaintext in the action-result `message`, but
the client was leaking it through the standard success-banner
pipeline — auto-dismiss in 4s, truncated, no copy button. Operators
were missing it.
- New `<HousekeepingPasswordReveal />` card mounted in the panel
header (between the status banner and tab content). Stays put
until manually dismissed.
- `useHousekeepingStore` gains a dedicated `passwordReveal` slot
(`{ userId, username, password }`) plus `revealPassword()` /
`clearPasswordReveal()` setters. Sensitive data, kept OUT of the
generic banner / toast pipeline.
- `useHousekeepingActions.resetUserPassword` no longer routes
through `wrap()` — it intercepts the result, lifts the
plaintext into the reveal slot, and uses a localizable success
key (`housekeeping.action.reset_password.done`) for the banner so
the password itself never lands there.
- Copy button uses `navigator.clipboard.writeText` in secure
contexts with a `document.execCommand('copy')` fallback for
http:// deployments. Confirmation icon flips to a checkmark for
~1.6s on success. The input is `select-all` + auto-select on
focus so Ctrl+C is also a manual fallback.
- 8 new i18n keys (EN + IT, .example + runtime UITexts.json5 /
UITexts.en.json5).
2. **Catalog admin cleanup ported from the PR branch.** The dev
branch was still carrying the catalog admin code (handlers, hooks,
store slots, i18n keys) even though the local renderer is on the
catalog-stripped `feat/housekeeping-packets` branch — typecheck
was breaking because the catalog composers no longer exist on the
linked renderer. Stripped here to match: 4 catalog actions
removed from `HousekeepingActionType`, `HousekeepingApi.ts`,
`useHousekeepingActions`, `useHousekeepingStore`. The CATALOG tab
id is gone from `HousekeepingTabId`. Catalog interfaces
(`IHousekeepingCatalogPage` / `IHousekeepingCatalogOffer`) are
dropped. 17 catalog i18n keys removed per locale. Two test files
updated to match.
Adds the Housekeeping in-client admin panel — a Modtools-adjacent
surface that runs entirely inside the React client, talking to the
emulator over the existing wire instead of a separate REST/CMS layer.
Surface:
- `src/components/housekeeping/` — panel shell + 5 tabs (Dashboard,
Users, Rooms, Economy, Audit). Each tab drives one domain of the
matching emulator handlers (find/sanction/admin/economy/catalog/
hotel-wide).
- `src/api/housekeeping/` — composer/parser orchestration:
`HousekeepingApi.ts` exposes 30+ typed actions, each one running
through `runHkAction()` which awaits the shared
`HousekeepingActionResultEvent` correlated by action key.
- `src/hooks/housekeeping/` — `useHousekeeping` (the public hook),
`useHousekeepingStore` (useBetween singleton: shared selection +
audit polling + sanction templates), `useHousekeepingActions`,
`useHousekeepingConfirm`.
- `src/api/nitro/awaitMessageEvent.ts` — Promise adapter over
`CommunicationManager.subscribeMessage` with a sync `select`
callback that snapshots the parser INSIDE the subscribe handler
before the renderer recycles the parser instance after the
Promise resolves.
- `public/configuration/housekeeping-texts-{en,it}.example` —
149 EN + 149 IT i18n keys under `housekeeping.*` for every panel
string + every server-side error slug the emulator may emit.
Wiring (additive only):
- `src/components/MainView.tsx` — `<HousekeepingView />` mounted
alongside `<ModToolsView />`.
- `src/api/index.ts`, `src/hooks/index.ts`, `src/api/nitro/index.ts`
— added the `housekeeping` and `awaitMessageEvent` re-exports.
Wire contract: pairs against the Arcturus PR (#120 on
duckietm/Arcturus-Morningstar-Extended) and the renderer PR (#77 on
duckietm/Nitro_Render_V3). Incoming events 9100..9129, outgoing
composers 9200..9207. Permission gate `acc_housekeeping` enforced
server-side; the panel is hidden client-side via
`housekeeping.enabled` in the runtime ui-config.
feat(loading): redesigned loader with progress bar, task labels, configurable assets + perf(build): granular code-split + preconnect hint for cold-load speed + docs: PERFORMANCE.md — client + server recipe for the 4s cold load
Loading screen overhaul:
- LoadingView: Nitro V3 logo flush top-left, loading.gif at viewport
centre, large progress bar (max 900px / 90vw, h-8, gradient + glow)
anchored bottom-centre with the percentage rendered inside the bar in
Poppins, plus a friendly stage label underneath. Logo + background +
progress bar colour overridable via renderer-config keys
(loading.logo.url, loading.background, loading.progress.color).
- App.tsx: wired a real loadingProgress (0->100) + loadingTask driven by
the boot pipeline: config init (10), renderer (20), per-warmup-task
bumps for AssetManager/Localization/AvatarRender/SoundManager (25->70),
session managers (78/85/92), Communication (98), ready (100). Each bump
carries a task label looked up via a new taskLabel(key, fallback)
helper so the Italian baseline ("Sto caricando il guardaroba",
"Connessione al server", ...) can be translated by editing
renderer-config; fallback keeps current strings if the key is missing.
- AvatarEffectsView: replace raw fetch(url).json() with
loadGamedata(url) so the effectmap root manifest (JSON5 with
// comments) parses correctly and supports the core/custom/seasonal
tier merge.
- fallbackToLogin: respect login.screen.enabled=false. When login is
disabled (SSO-only deployments), init failures now route to
showSessionExpired() (home + diagnostic) instead of rendering an empty
LoginView placeholder.
- scripts/write-asset-loader.mjs: the pre-React shell rendered into
#root before the JS bundle takes over was a light-blue login skeleton
(linear gradient + two grey rectangles) producing a visible flash
before the real loader appeared. Replaced with the same
radial-gradient the LoadingView paints — the handoff is now invisible.
- renderer-config.example: document the 13 loader keys so operators can
copy & translate.
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).
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.
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.
Lets the operator pick between strict JSON (legacy) and JSON5 for every
configuration file consumed by Nitro and the renderer.
- scripts/configure-json.mjs: interactive prompt (JSON5 recommended),
with --if-missing and --non-interactive flags for CI use
- package.json: yarn configure / prestart / prebuild hooks
- vite.config.mjs: reads .nitro-build.json (or NITRO_JSON_MODE env) and
injects the compile-time constant __NITRO_JSON_MODE__ via define
- src/bootstrap.ts: routes client-mode.json parsing through the
selected mode
- .gitignore: ignore the per-deployment .nitro-build.json
- README: full usage and override section
- public/configuration assets regenerated by the updated prebuild flow
The renderer side (@nitrots/utils JsonParser) is updated in the
companion Nitro_Render_V3 commit on the dev branch.
The example data has been provided in /Content-Gamedata so you could place it in /gamadata or anything you like.
Do not forget the render-config.json to update :
"login.health.method": "GET",
"login.news.url": "${asset.url}/news/news.json",
Adds a "Cards" tab to the Profile Background picker (BackgroundsView)
that selects a pattern applied to the entire user info card and the
extended profile container, in addition to the existing avatar-pad
background/stand/overlay layers.
- AvatarInfoUser/Utilities: propagate cardBackgroundId from RoomUserData.
- InfoStandWidgetUserView: stateful cardBackgroundId, applied as
.profile-card-background.card-background-{id} on the outer Column
with bg-color suppressed when active.
- UserContainerView: same class on the wrapper of the extended profile.
- BackgroundsView: 4th tab "cards" backed by cards.data config
(falls back to backgrounds.data); sends 4-id message via the
extended sendBackgroundMessage signature.
- ui-config.example: cards.data dataset (15 entries).
- BackgroundsView.css: 188 .card-background-{N} rules cloned from
background-{N} (repeat-tiled) plus 15 CSS-pattern overrides for the
provisional dataset (gradients, stripes, dots, grid, checker).
Backend (AuthHttpHandler):
- New users_remember_tokens table stores sha256 hex of the raw token
so the DB never holds a usable credential. Seed file adds the table
and a login.remember.duration.days setting (default 30).
- /api/auth/login accepts "remember": true. On success, issues a fresh
32-byte base64url token, stores the hash, returns the raw token.
- New POST /api/auth/remember: accepts the raw token, looks up by hash,
on a valid hit mints a fresh SSO ticket, rotates the token (deletes
the consumed one and issues a new one), returns both to the client.
No Turnstile - it's an automated trusted-device flow.
- /api/auth/logout also accepts rememberToken and deletes that single
row so other devices keep their tokens.
Frontend:
- LoginView: "Remember me" checkbox (key login.remember_me already in
ExternalTexts). Enabling it persists the returned rememberToken in
localStorage.nitro.remember.token.
- App.tsx: before deciding to show the login screen, try a silent POST
to /api/auth/remember with the stored token. On 200, inject the
returned ssoTicket into window.NitroConfig and proceed to the
authenticated flow; on 401, forget the token and show login.
- PurseView logout: sends the stored rememberToken in the body so the
server can delete it, and clears localStorage before reload.