Adds a `fun` category to the operator command registry, reachable identically
from site chat and Discord:
- text: bonk, hug, slap, 8ball, roll, coin, ship, rate, uwu, wanted
- counters: bonkboard, pet, snitch
- hardware: honk, boo, spin, disco, vibecheck
Supporting pieces:
- `permission: 'public'` in the registry. The dispatcher previously decided
non-admin access with a hardcoded chain of `action !== '...'` comparisons, so
every new public command needed a dispatcher edit. That chain is replaced by a
registry lookup plus SELF_GATED_ACTIONS, which names the commands that enforce
their own permissions internally (goal/reason are read-public write-admin;
verify/deter reject non-lockdown-admins themselves). Existing behavior for
every pre-existing command is unchanged.
- `cooldowns.js`, a per-actor per-command in-memory gate. Site chat's own rate
limit is per-socket-per-message and does not bound a specific command, so
without this one person could turn `rs honk` into a siren. Site chat rebuilds
its router per message, so the gate is created at module scope there and
injected.
- `funStatsService`, a small JSON store for the persistent tallies. Counters are
keyed by an actor key spanning transports (`user:<id>` / `discord:<id>`), and a
Discord id has no row in `users`, so `user_feature_state` could not hold them
without violating its foreign key.
Safety notes:
- `issueCommand` is the raw rover transport and performs none of the ownership,
deterrence, or private-safety checks the socket `command` handler applies, so
honk and spin re-check `canDrive` themselves and spin re-applies
`applyPrivateDriveSafety`. Both are therefore site-chat only: a Discord message
has no socket and can never satisfy those checks.
- `boo` speaks a canned taunt rather than caller-supplied text, so it cannot
become an unmoderated TTS channel aimed at whoever is nearest a rover.
- `disco` obeys the existing room-light lock and the homeAssistant feature gate.
- The whole fun category is suspended in lockdown mode.
- Mute and deterrence already stop command-shaped chat before the router runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The identity resolver's fuzzy-miss error was labelled "Selector", which is
internal jargon — the operator running `rs gain grant <vip>` or `rs kick
<user>` typed a username, not a "selector". Relabel that one message to
"User" so the chat reply reads plainly.
The sibling "Selector matched multiple records." and "Selector required."
messages are intentionally left alone for now.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Admins can now run 'rs gain list|grant <vip>|revoke <vip>' from web chat or
Discord. Grant matches only against the verified list and revoke only
against current holders, so a nickname shared with an unverified visitor
reports not-found rather than resolving to someone ineligible. The action
joins the moderation set so lockdown narrows it to lockdown admins.
Also extracts the ceiling math into audioLevelsService/gainMath.js and
covers both it and the command with node:test suites.
Co-Authored-By: Claude <noreply@anthropic.com>
Every user now gets a personal 0-1 volume for horn, TTS, and mic forward.
The value is stored as a fraction of the ceiling that applies to them, so
lowering the global admin gain quiets everyone immediately instead of
leaving stale absolute values behind.
Ceilings resolve in three layers: the global admin gain is the default
ceiling; the audioGainBoost flag raises it to an admin-editable hard cap
(default 0.5x horn, 0.8x TTS, 0.4x forward); Math.max keeps the flag from
ever lowering a ceiling if the global gain is set higher than a cap.
Preferences live in identity feature state rather than a cookie so they
follow the user and cannot be raised client-side. The rover exposes gain
as three ALSA masters, so the resolved gains pushed to a rover are those
of the socket currently holding audio control -- re-pushed on driver
join/leave and every turn rotation.
Co-Authored-By: Claude <noreply@anthropic.com>
Adds an audio_gain_boost_enabled status column (plus _at/_by audit
fields) to user_status, exposed as user.audioGainBoost and copied onto
sockets as socket.data.hasAudioGainBoost. The flag marks VIPs allowed to
raise their personal horn/TTS/mic gain ceiling past the global admin gain
settings.
Grant/revoke goes through verificationService so socket flags refresh and
an event is published. Resolution is restricted to verified users, so an
unverified visitor sharing a nickname can never be matched.
Co-Authored-By: Claude <noreply@anthropic.com>