Runtime-loaded visual re-skin system (no client rebuild, real themes never
hit git). A theme = a folder on the server (theme.base.url) with a manifest +
CSS "pieces"; each piece is toggled from Settings > Themes (checkboxes). A
broken/404 piece auto-falls back to the default (per piece). Hotel-wide default
via ui-config theme.default (+ theme.default.pieces), per-user override in
localStorage (same pattern as the catalog style toggle).
- api/theme/ThemeManager: fetch index/manifest + inject/remove <link> + fallback
- hooks/theme/useThemes: state + persist + default-from-config + live apply
- components/theme/ThemeApplier: applies on boot (mounted in MainView)
- UserSettings: General/Themes tabs with theme selector + per-piece checkboxes
- custom-themes/: reference template (demo theme "Neon Viola" + README)
- .gitignore: public/custom-themes/ (real themes are never committed)
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.
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.
UiSettingsContext referenced UiSettingsLoadComposer /
UiSettingsSaveComposer / UiSettingsDataEvent — none of which exist on
the renderer, and the corresponding Arcturus packet handlers don't
exist either (grep across the emulator turns up zero matches for
'UiSettings'). The feature is real (theme color/image stored in
localStorage works) but the cross-device sync was wired against a
non-existent server endpoint.
Strip the server-bound code path: settings keep persisting to
localStorage as before. The full sync becomes a follow-up that will
need both renderer composer classes AND the Arcturus packet handler
landing together.
Also re-export src/api/ui-settings/ from src/api/index so
InterfaceImageTabView / InterfaceColorTabView can import useUiSettings
+ PRESET_COLORS / THEME_PRESETS via the root barrel as the rest of the
codebase does.
Net tsgo error count: -7 (3 from UiSettingsContext imports + 4 from
InterfaceColor/ImageTabView consumers).
- RGBA color picker with live preview (debounce 50ms)
- 30 preset colors + 12 theme presets (Ocean, Forest, Sunset, Royal, etc.)
- Header image selection from configurable image library
- Export/Import theme as JSON via clipboard
- CSS variable theming across all UI elements: NitroCard headers/tabs,
context menus, buttons (primary/dark/gray), InfoStand, toolbar,
room tools, purse, progress bars, sliders
- All elements use var(--name, fallback) for zero visual change when default
- Smooth 0.3s CSS transitions on theme change
- Server-side persistence via WebSocket (packets 10047/10048)
- Integrated Color/Image tabs into BackgroundsView panel
- All strings use LocalizeText() for i18n support
- Settings persisted in localStorage + server sync with 1s debounce
- Added react-colorful dependency